1. 简介
访问控制列表(ACL) 是附加到对象上的权限列表。ACL 明确规定了哪些身份(用户或角色)对给定对象拥有哪些操作权限。
Spring Security ACL 是 Spring 提供的一个组件,专门用于实现领域对象安全。简单来说,Spring ACL 能帮我们为特定用户/角色在单个领域对象上定义权限——而不是像传统操作那样进行全局性的权限控制。
举个例子:
- 拥有 Admin 角色的用户可以查看(READ)和编辑(WRITE)中央公告箱中的所有消息
- 普通用户只能查看与自己相关的消息,不能编辑
- 拥有 Editor 角色的用户可以查看和编辑部分特定消息
这种场景下,不同用户/角色对每个对象都有不同的权限。Spring ACL 正好能搞定这种需求。本文将带你一步步搭建 Spring ACL 的基础权限校验功能。
2. 配置
2.1. ACL 数据库结构
使用 Spring ACL 必须在数据库中创建四个核心表:
1. ACL_CLASS
表
存储领域对象的类名,包含字段:
ID
:主键CLASS
:领域对象的完整类名,例如:com.baeldung.acl.persistence.entity.NoticeMessage
2. ACL_SID
表
用于全局标识系统中的所有主体(用户)或权限(角色),包含字段:
ID
:主键SID
:用户名或角色名(SID = Security Identity)PRINCIPAL
:0 或 1(0 表示角色,1 表示用户)
3. ACL_OBJECT_IDENTITY
表
存储每个唯一领域对象的信息,包含字段:
ID
:主键OBJECT_ID_CLASS
:对象类 ID(关联ACL_CLASS
表)OBJECT_ID_IDENTITY
:领域对象的主键值PARENT_OBJECT
:父对象 ID(实现权限继承)OWNER_SID
:对象所有者 ID(关联ACL_SID
表)ENTRIES_INHERITING
:是否继承父对象的权限(0/1)
4. ACL_ENTRY
表
存储具体权限分配,包含字段:
ID
:主键ACL_OBJECT_IDENTITY
:对象 ID(关联ACL_OBJECT_IDENTITY
表)ACE_ORDER
:权限条目序号SID
:用户/角色 ID(关联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 需要缓存来存储对象标识和权限条目,这里使用
spring-context
提供的ConcurrentMapCache
。
非 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
启用基于表达式的访问控制(SpEL),并配置支持 ACL 的表达式处理器:
@Bean
public MethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
AclPermissionEvaluator permissionEvaluator = new AclPermissionEvaluator(aclService());
expressionHandler.setPermissionEvaluator(permissionEvaluator);
return expressionHandler;
}
关键配置:将 AclPermissionEvaluator
注入到 DefaultMethodSecurityExpressionHandler
。该评估器需要 MutableAclService
从数据库加载权限设置。
为简化实现,使用 Spring 提供的 JdbcMutableAclService
:
@Bean
public JdbcMutableAclService aclService() {
return new JdbcMutableAclService(dataSource, lookupStrategy(), aclCache());
}
JdbcMutableAclService
依赖三个核心组件:
DataSource
(用于 JDBC 操作)LookupStrategy
(优化数据库查询)AclCache
(缓存权限条目和对象标识)
继续配置这些组件:
@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 默认使用 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
→ 只返回当前用户有READ
权限的NoticeMessage
findById()
方法执行后触发@PostAuthorize
→ 仅当用户有READ
权限时才返回对象,否则抛AccessDeniedException
save()
方法执行前触发@PreAuthorize
→ 检查用户是否有WRITE
权限,无权限则抛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. 测试场景设计
创建两个用户(manager
, hr
)和一个角色(ROLE_EDITOR
),初始化 acl_sid
表:
INSERT INTO acl_sid (principal, sid) VALUES
(1, 'manager'),
(1, 'hr'),
(0, 'ROLE_EDITOR');
声明领域对象类 NoticeMessage
,并插入三条测试数据:
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
用户:仅对第二条消息(id=2)有READ
权限
🔑 权限掩码说明:
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), -- manager: READ on msg1
(1, 2, 1, 2, 1, 1, 1), -- manager: WRITE on msg1
(1, 3, 3, 1, 1, 1, 1), -- ROLE_EDITOR: READ on msg1
(2, 1, 2, 1, 1, 1, 1), -- hr: READ on msg2
(2, 2, 3, 1, 1, 1, 1), -- ROLE_EDITOR: READ on msg2
(3, 1, 3, 1, 1, 1, 1), -- ROLE_EDITOR: READ on msg3
(3, 2, 3, 2, 1, 1, 1); -- ROLE_EDITOR: WRITE on msg3
4.2. 测试用例验证
场景1:manager
用户查询消息
预期结果:仅返回第一条消息(有 READ 权限)
@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());
}
场景2:ROLE_EDITOR
角色查询消息
预期结果:返回全部三条消息(均有 READ 权限)
@Test
@WithMockUser(roles = {"EDITOR"})
public void
givenRoleEditor_whenFindAllMessage_thenReturn3Message(){
List<NoticeMessage> details = repo.findAll();
assertNotNull(details);
assertEquals(3,details.size());
}
场景3:manager
用户修改第一条消息
预期结果:成功修改(有 WRITE 权限)
@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());
}
场景4:ROLE_EDITOR
尝试修改第一条消息
预期结果:抛 AccessDeniedException
(无 WRITE 权限)
@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);
}
场景5: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 的基础配置和使用要点:
📌 核心概念:
- ACL 通过特定表结构管理对象、用户/角色及权限关系
- 所有权限操作必须通过
AclService
进行(后续文章将详解其 CRUD 操作)
⚠️ 注意事项:
- 默认权限限制在
BasePermission
类中预定义的权限类型 - 权限继承功能需谨慎使用(通过
PARENT_OBJECT
和ENTRIES_INHERITING
控制)
💡 完整实现代码可在 GitHub 获取。
踩坑提示:生产环境建议使用 EhCache 替代ConcurrentMapCache
以获得更好的性能表现。