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 依赖三个核心组件:

  1. DataSource(用于 JDBC 操作)
  2. LookupStrategy(优化数据库查询)
  3. 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);

✅ 注解执行逻辑:

  1. findAll() 方法执行后触发 @PostFilter
    → 只返回当前用户有 READ 权限的 NoticeMessage
  2. findById() 方法执行后触发 @PostAuthorize
    → 仅当用户有 READ 权限时才返回对象,否则抛 AccessDeniedException
  3. 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_OBJECTENTRIES_INHERITING 控制)

💡 完整实现代码可在 GitHub 获取。
踩坑提示:生产环境建议使用 EhCache 替代 ConcurrentMapCache 以获得更好的性能表现。


原始标题:An Introduction to Spring Security ACL

« 上一篇: Java 可变参数详解
» 下一篇: Mule ESB 入门指南