1. 簡介
訪問控制列表 (ACL) 是一個附加在對象上的權限列表。一個 ACL 指定了哪些身份被授予在給定對象上的哪些操作。
Spring Security ACL
是 一個 Spring 組件,它支持 領域對象安全。 簡單來説,Spring ACL 幫助定義了特定用户/角色在單個領域對象上的權限——而不是像傳統那樣,在操作級別上進行設置。例如,擁有 Admin 角色的用户可以查看(READ)和編輯(WRITE)所有 中央通知箱 中的消息,而普通用户只能查看與他們相關的消息,並且不能進行編輯。 另一方面,擁有 Editor 角色的其他用户可以查看和編輯某些特定的消息。
因此,不同用户/角色對每個特定對象擁有不同的權限。 在這種情況下,Spring ACL 能夠實現此任務。 在本文中,我們將探討如何使用 Spring ACL 設置基本權限檢查。
2. 配置
2.1. ACL 數據庫
要使用 Spring Security ACL,我們需要在數據庫中創建四個必選表。
第一個表是 ACL_CLASS,用於存儲域對象的類名,包含以下列:
- ID
- CLASS: 受保護的域對象類名,例如: com.baeldung.acl.persistence.entity.NoticeMessage
其次,我們需要 ACL_SID 表,以便在系統範圍內普遍識別任何原則或權限。該表需要:
- ID
- SID: 用户名或角色名稱。 SID 代表 Security Identity
- PRINCIPAL: 0 或 1,指示對應的 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 的 ID,鏈接到 ACL_SID 表
- ENTRIES_INHERITING: ACL Entries 是否從父對象繼承(ACL Entries 定義在 ACL_ENTRY 表中)
最後,ACL_ENTRY 存儲為每個 SID 在 Object Identity 上分配的單個權限:
- ID
- ACL_OBJECT_IDENTITY: 指定對象身份,鏈接到 ACL_OBJECT_IDENTITY 表
- ACE_ORDER: 當前條目在 ACL entries 列表中對應的 Object Identity 的順序
- SID: 目標 SID,授予或拒絕權限,鏈接到 ACL_SID 表
- MASK: 表示授予或拒絕的實際權限的整數位掩碼
- GRANTING: 值為 1 表示授予,值為 0 表示拒絕
- AUDIT_SUCCESS 和 AUDIT_FAILURE: 用於審計目的
2.2. 依賴
為了能夠在我們的項目中成功使用 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 需要一個緩存來存儲對象標識和權限控制條目,因此我們將利用 ConcurrentMapCache,該緩存由 spring-context 提供。
在不使用 Spring Boot 的情況下,我們需要明確指定版本。可以在 Maven Central 上找到它們:spring-security-acl,spring-security-config,spring-context-support。
2.3. 相關的 ACL 配置
為了確保所有返回已安全域對象的方法,或對對象進行修改,需要啓用 全局方法安全。
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class AclMethodSecurityConfiguration
extends GlobalMethodSecurityConfiguration {
@Autowired
MethodSecurityExpressionHandler
defaultMethodSecurityExpressionHandler;
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return defaultMethodSecurityExpressionHandler;
}
}讓我們也啓用基於表達式的訪問控制,通過將prePostEnabled設置為true來使用 Spring 表達式語言 (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 Entry 和 Object Identity)。
再次強調,為了簡化,我們使用提供的 BasicLookupStrategy 和 EhCacheBasedAclCache。
@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() 方法後觸發,確保只返回當前用户對該 NoticeMessage 對象具有 READ 權限時才返回該對象。 如果沒有,系統將拋出 AccessDeniedException。
另一方面,系統在調用 save() 方法之前觸發 @PreAuthorize 註解。 它將決定相應的方法是否允許執行。 如果沒有,將拋出 AccessDeniedException。
4. 實際應用
現在我們將使用 JUnit 測試所有這些配置。 我們將使用 H2 數據庫以保持配置儘可能簡單。
我們需要添加:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>4.1. 場景
在當前場景中,我們將有兩位用户(經理、人力資源)和一個用户角色(角色編輯),因此我們的 acl_sid 將為:
INSERT INTO acl_sid (principal, sid) VALUES
(1, 'manager'),
(1, 'hr'),
(0, 'ROLE_EDITOR');然後,我們需要在 acl_class 中聲明 NoticeMessage 類。並將三個 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);最初,我們授予用户 manager 在第一個對象 (id =1) 上 READ 和 WRITE 權限。與此同時,任何具有 ROLE_EDITOR 角色的用户都將擁有所有三個對象上的 READ 權限,但僅在第三個對象 (id=3) 上擁有 WRITE 權限。此外,用户 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, 2, 1, 1, 1);4.2. 測試用例
首先,我們嘗試調用 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 用户可以通過 ID 查找第二個消息,但無法更新它:
@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 操作。
默認情況下,我們受到 BasePermission 類中預定義的權限的限制。