Spring Security 基於權限控制(ACL)簡介

Spring Security
Remote
0
11:26 PM · Nov 29 ,2025

1. 簡介

訪問控制列表(ACL) 是一個附加到對象的權限列表。一個 ACL 指定了哪些身份被授予在給定對象上的哪些操作。

Spring Security 訪問控制列表 是一個 支持 領域對象安全Spring 組件。 簡單來説,Spring ACL 幫助定義了特定用户/角色在單個領域對象上的權限——而不是像典型的按操作級別那樣。

例如,具有 管理員 角色的用户可以查看 (讀取) 並編輯 (寫入) 所有 中央通知箱 中的消息,而普通用户只能查看與他們相關的消息,並且不能編輯。 另一方面,具有 編輯 角色的其他用户可以查看和編輯某些消息。

因此,不同用户/角色對每個特定對象具有不同的權限。 在這種情況下,Spring ACL 能夠完成此任務。 在本文中,我們將探討如何使用 Spring ACL 設置基本的權限檢查。

2. Configuration

2.1. ACL Database

為了使用 Spring Security ACL,我們需要在數據庫中創建四個必填表。

第一個表是 ACL_CLASS,它存儲了域對象的類名,包含以下列:

  • ID
  • CLASS: 存儲受保護的域對象類名,例如: com.baeldung.acl.persistence.entity.NoticeMessage

第二個表是 ACL_SID,它允許我們對系統中的任何原則或權限進行統一識別。該表需要:

  • ID
  • SID: 用户名或角色名稱。 SID 代表 Security Identity
  • PRINCIPAL: 設置為 01,用於指示相應的 SID 是否是一個原則(用户,例如 mary, mike, jack…)還是一個權限(角色,例如 ROLE_ADMIN, ROLE_USER, ROLE_EDITOR…

下一個表是 ACL_OBJECT_IDENTITY,它存儲了每個唯一域對象的的信息:

  • ID
  • OBJECT_ID_CLASS: 定義域對象類,鏈接到 ACL_CLASS
  • OBJECT_ID_IDENTITY: 域對象可以存儲在多個表中,具體取決於其類。因此,此字段存儲目標對象的鍵
  • PARENT_OBJECT: 指定此 Object Identity 在表中父對象
  • OWNER_SID: 對象的擁有者 SID,鏈接到 ACL_SID
  • ENTRIES_INHERITING: ACL Entries 是否從父對象繼承,ACL Entries 定義在 ACL_ENTRY 表中

最後,ACL_ENTRY 存儲了為每個 SIDObject Identity 上分配的單獨權限:

  • ID
  • ACL_OBJECT_IDENTITY: 指定對象身份,鏈接到 ACL_OBJECT_IDENTITY
  • ACE_ORDER: 當前條目的 ACL entries 列表中順序
  • SID: 目標 SID,授予或拒絕權限,鏈接到 ACL_SID
  • MASK: 表示授予或拒絕的實際權限的整數掩碼
  • GRANTING: 值為 1 表示授予,值為 0 表示拒絕
  • AUDIT_SUCCESSAUDIT_FAILURE:用於審計目的

2.2. Dependency

為了在我們的項目中使用 Spring ACL,首先需要定義我們的依賴項:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-acl</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
</dependency>

Spring ACL 需要一個緩存來存儲 Object IdentityACL entries,因此我們將使用 ConcurrentMapCache,該緩存由 spring-context 提供。

如果不使用 Spring Boot,則需要顯式添加版本。可以在 Maven Central 上找到這些版本:spring-security-acl, spring-security-config, spring-context-support

2.3. ACL-Related Configuration

我們需要啓用所有返回受保護域對象的方法,或修改對象,通過啓用 Global Method Security

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class AclMethodSecurityConfiguration 
  extends GlobalMethodSecurityConfiguration {

    @Autowired
    MethodSecurityExpressionHandler 
      defaultMethodSecurityExpressionHandler;

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        return defaultMethodSecurityExpressionHandler;
    }
}

我們還需要啓用 Expression-Based Access Control,通過將 prePostEnabled 設置為 true 來使用 Spring Expression Language (SpEL)。 此外,我們需要一個具有 ACL 支持的表達式處理器:

@Bean
public MethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler() {
    DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
    AclPermissionEvaluator permissionEvaluator = new AclPermissionEvaluator(aclService());
    expressionHandler.setPermissionEvaluator(permissionEvaluator);
    return expressionHandler;
}

因此,我們將 AclPermissionEvaluator 分配給 DefaultMethodSecurityExpressionHandler。 評估器需要一個 MutableAclService 來從數據庫中加載權限設置和域對象的定義。

為了簡化,我們將使用提供的 JdbcMutableAclService

@Bean 
public JdbcMutableAclService aclService() { 
    return new JdbcMutableAclService(dataSource, lookupStrategy(), aclCache()); 
}

正如其名稱所示,JdbcMutableAclService 使用 JDBCTemplate 來簡化數據庫訪問。 它需要 DataSource (用於 JDBCTemplate)LookupStrategy (在查詢數據庫時提供優化的查找) 和 AclCache (緩存 ACL EntriesObject Identity)

為了簡化,我們將使用提供的 BasicLookupStrategyEhCacheBasedAclCache

@Autowired
DataSource dataSource;

@Bean
public AclAuthorizationStrategy aclAuthorizationStrategy() {
    return new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ROLE_ADMIN"));
}

@Bean
public PermissionGrantingStrategy permissionGrantingStrategy() {
    return new DefaultPermissionGrantingStrategy(new ConsoleAuditLogger());
}

@Bean
public SpringCacheBasedAclCache aclCache() {
    final ConcurrentMapCache aclCache = new ConcurrentMapCache("acl_cache");
    return new SpringCacheBasedAclCache(aclCache, permissionGrantingStrategy(), aclAuthorizationStrategy());
}

@Bean 
public LookupStrategy lookupStrategy() { 
    return new BasicLookupStrategy(
      dataSource, 
      aclCache(), 
      aclAuthorizationStrategy(), 
      new ConsoleAuditLogger()
    ); 
}

在這裏,AclAuthorizationStrategy 負責確定當前用户是否擁有在特定對象上所需的權限。

它需要 PermissionGrantingStrategy,該策略定義瞭如何確定權限是否授予給特定的 SID

3. 使用 Spring ACL 進行方法安全保障

至今,我們已經完成了所有必要的配置 現在我們可以將所需的檢查規則應用於我們的安全方法。

默認情況下,Spring ACL 引用BasePermission 類作為所有可用權限的基礎。 基本上,我們有READ, WRITE, CREATE, DELETE ADMINISTRATION 權限。

讓我們嘗試定義一些安全規則:

@PostFilter("hasPermission(filterObject, 'READ')")
List<NoticeMessage> findAll();
    
@PostAuthorize("hasPermission(returnObject, 'READ')")
NoticeMessage findById(Integer id);
    
@PreAuthorize("hasPermission(#noticeMessage, 'WRITE')")
NoticeMessage save(@Param("noticeMessage")NoticeMessage noticeMessage);

在執行findAll() 方法後,@PostFilter 將被觸發。 所需的規則hasPermission(filterObject, ‘READ’), 意味着只返回當前用户在NoticeMessage 上具有READ 權限的那些對象。

同樣,@PostAuthorize 在執行findById() 方法後被觸發,確保只返回當前用户在它上具有READ權限的NoticeMessage 對象。 否則,系統將拋出AccessDeniedException

在另一方面,系統在調用save() 方法之前觸發@PreAuthorize 註解。 它將決定相應的方法是否允許執行或執行,如果執行,則拋出AccessDeniedException

4. In Action

現在我們將使用 JUnit測試所有這些配置。我們將使用 H2 數據庫以保持配置儘可能簡單。

我們需要添加:


<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
</dependency>

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-test</artifactId>
  <scope>test</scope>
</dependency>

4.1. The Scenario

在這個場景中,我們有兩個用户(manager, hr)和一個用户角色(ROLE_EDITOR),因此我們的acl_sid將是:


INSERT INTO acl_sid (principal, sid) VALUES
  (1, 'manager'),
  (1, 'hr'),
  (0, 'ROLE_EDITOR');

然後,我們需要聲明 NoticeMessage類在 acl_class中。並且三個 NoticeMessage類的實例將被插入到 system_message中。

此外,相應的記錄必須在 acl_object_identity中聲明:


INSERT INTO acl_class (id, class) VALUES
  (1, 'com.baeldung.acl.persistence.entity.NoticeMessage');

INSERT INTO system_message(id,content) VALUES 
  (1,'First Level Message'),
  (2,'Second Level Message'),
  (3,'Third Level Message');

INSERT INTO acl_object_identity 
  (object_id_class, object_id_identity, 
  parent_object, owner_sid, entries_inheriting) 
  VALUES
  (1, 1, NULL, 3, 0),
  (1, 2, NULL, 3, 0),
  (1, 3, NULL, 3, 0);

最初,我們授予 READ WRITE 權限於第一個對象(id =1)給用户 manager。同時,任何擁有 ROLE_EDITOR角色的用户將擁有所有三個對象上的 READ權限,但僅擁有第三個對象的 WRITE權限(id=3)。此外,用户 hr將僅擁有第二個對象的 READ權限。

這裏,由於我們使用默認的 Spring ACL BasePermission 類進行權限檢查,因此 READ 權限的掩碼值為1,WRITE 權限的掩碼值為2。我們的 acl_entry數據如下:


INSERT INTO acl_entry 
  (acl_object_identity, ace_order, 
  sid, mask, granting, audit_success, audit_failure) 
  VALUES
  (1, 1, 1, 1, 1, 1, 1),
  (1, 2, 1, 2, 1, 1, 1),
  (1, 3, 3, 1, 1, 1, 1),
  (2, 1, 2, 1, 1, 1, 1),
  (2, 2, 3, 1, 1, 1, 1),
  (3, 1, 3, 1, 1, 1, 1),
  (3, 2, 3, 1, 1, 1, 1);

4.2. Test Case

首先,我們嘗試調用 findAll方法。

由於我們的配置,該方法只返回那些用户具有 READ權限的 NoticeMessage對象。

因此,我們期望的結果列表只包含第一個消息:


@Test
@WithMockUser(username = "manager")
public void 
  givenUserManager_whenFindAllMessage_thenReturnFirstMessage(){
    List<NoticeMessage> details = repo.findAll(); 

    assertNotNull(details);
    assertEquals(1,details.size());
    assertEquals(FIRST_MESSAGE_ID,details.get(0).getId());
}

然後,我們嘗試使用任何擁有 ROLE_EDITOR角色的用户調用相同的該方法。注意,在這種情況下,這些用户具有所有三個對象的 READ權限。

因此,我們期望的結果列表將包含所有三個消息:


@Test
@WithMockUser(roles = {"EDITOR"})
public void 
  givenRoleEditor_whenFindAllMessage_thenReturn3Message(){
    List<NoticeMessage> details = repo.findAll(); 

    assertNotNull(details);
    assertEquals(3,details.size());
}

接下來,使用 manager用户,我們將嘗試通過 ID 獲取第一個消息並更新其內容——這應該一切順利:


@Test
@WithMockUser(username = "manager")
public void 
  givenUserManager_whenFind1stMessageByIdAndUpdateItsContent_thenOK(){
    NoticeMessage firstMessage = repo.findById(FIRST_MESSAGE_ID);
    assertNotNull(firstMessage);
    assertEquals(FIRST_MESSAGE_ID,firstMessage.getId()); 

    firstMessage.setContent(EDITTED_CONTENT);
    repo.save(firstMessage); 

    NoticeMessage editedFirstMessage = repo.findById(FIRST_MESSAGE_ID); 

    assertNotNull(editedFirstMessage);
    assertEquals(FIRST_MESSAGE_ID,editedFirstMessage.getId());
    assertEquals(EDITTED_CONTENT,editedFirstMessage.getContent());
}

但是,如果任何擁有 ROLE_EDITOR角色的用户更新第一個消息的內容——我們的系統將拋出 AccessDeniedException


@Test(expected = AccessDeniedException.class)
@WithMockUser(roles = {"EDITOR"})
public void 
  givenRoleEditor_whenFind1stMessageByIdAndUpdateContent_thenFail(){
    NoticeMessage firstMessage = repo.findById(FIRST_MESSAGE_ID); 

    assertNotNull(firstMessage);
    assertEquals(FIRST_MESSAGE_ID,firstMessage.getId()); 

    firstMessage.setContent(EDITTED_CONTENT);
    repo.save(firstMessage);
}

同樣,hr用户可以找到第二個消息,但無法更新它:


@Test
@WithMockUser(username = "hr")
public void givenUsernameHr_whenFindMessageById2_thenOK(){
    NoticeMessage secondMessage = repo.findById(SECOND_MESSAGE_ID);
    assertNotNull(secondMessage);
    assertEquals(SECOND_MESSAGE_ID,secondMessage.getId());
}

@Test(expected = AccessDeniedException.class)
@WithMockUser(username = "hr")
public void givenUsernameHr_whenUpdateMessageWithId2_thenFail(){
    NoticeMessage secondMessage = new NoticeMessage();
    secondMessage.setId(SECOND_MESSAGE_ID);
    secondMessage.setContent(EDITTED_CONTENT);
    repo.save(secondMessage);
}

5. 結論

我們已經回顧了 Spring ACL 的基本配置和使用方法,本文檔中。

正如我們所知,Spring ACL 需要特定的表來管理對象、原則/權限和權限設置。所有與這些表進行的交互,尤其是更新操作,都必須通過 AclService 進行。我們將在一篇未來的文章中探討這個服務,用於基本的 CRUD 操作。

默認情況下,我們受到 BasePermissio 類中預定義的權限的限制。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.