Spring Security ACL(访问控制列表):细粒度权限模型设计与实现

Spring Security ACL:细粒度权限模型设计与实现

大家好,今天我们来深入探讨Spring Security ACL(访问控制列表),这是一个强大的工具,用于实现细粒度的权限控制。与基于角色的访问控制(RBAC)不同,ACL允许我们针对单个领域对象实例设置权限,从而实现更精细、更灵活的权限管理。

1. 为什么需要细粒度权限控制?

传统的基于角色的访问控制(RBAC)模型,将权限授予角色,用户再被分配到角色,从而间接获得权限。这种模型在很多场景下都足够使用,但当权限需求变得复杂时,例如:

  • 特定用户的特定对象的特殊权限: 允许某个用户编辑某个特定的文档,即使他没有编辑所有文档的权限。
  • Owner权限: 允许对象的所有者拥有完全控制权,而其他用户只能读取。
  • 协作权限: 允许一组用户对特定对象进行协作编辑,而其他用户只能查看。

RBAC模型就显得力不从心。我们需要一种更精细的模型,能够针对单个对象实例分配权限,这就是ACL发挥作用的地方。

2. Spring Security ACL 核心概念

Spring Security ACL基于以下核心概念:

  • AclService: 核心接口,负责管理ACL信息,例如创建、读取、更新和删除ACL条目。Spring Security提供了JdbcMutableAclService,它使用数据库来存储ACL信息。
  • Acl: 代表特定领域对象的访问控制列表。它包含一系列的Ace (Access Control Entry)。
  • Ace (Access Control Entry): ACL中的权限条目,定义了谁(Sid)拥有什么权限(Permission)访问哪个对象(通过ObjectIdentity)。
  • Sid (Security Identity): 代表安全主体,可以是用户(PrincipalSid)或角色(GrantedAuthoritySid)。
  • ObjectIdentity: 唯一标识一个领域对象。它包含对象的类型(java.lang.Class)和对象的ID(java.io.Serializable)。
  • Permission: 代表可以执行的操作,例如读取、写入、删除、管理等。Spring Security提供了BasePermission,预定义了一些常用的权限。

用表格总结如下:

概念 描述
AclService 负责管理ACL信息,例如创建、读取、更新和删除ACL条目。
Acl 代表特定领域对象的访问控制列表。
Ace ACL中的权限条目,定义了谁(Sid)拥有什么权限(Permission)访问哪个对象(通过ObjectIdentity)。
Sid 代表安全主体,可以是用户(PrincipalSid)或角色(GrantedAuthoritySid)。
ObjectIdentity 唯一标识一个领域对象。
Permission 代表可以执行的操作,例如读取、写入、删除、管理等。

3. Spring Security ACL 的数据库结构

JdbcMutableAclService 默认使用以下表来存储 ACL 信息:

  • acl_sid: 存储安全主体 (Sid) 信息,例如用户和角色。
  • acl_class: 存储领域对象的类型 (ObjectIdentity)。
  • acl_object_identity: 存储领域对象的唯一标识 (ObjectIdentity) 和对应的 ACL 信息。
  • acl_entry: 存储访问控制条目 (Ace),将 Sid、ObjectIdentity 和 Permission 关联起来。

以下是这些表的简要结构:

acl_sid 表:

列名 类型 描述
id BIGINT 主键,自增
principal BOOLEAN 是否是用户 (Principal)
sid VARCHAR(100) 用户名或角色名

acl_class 表:

列名 类型 描述
id BIGINT 主键,自增
class VARCHAR(255) 领域对象的完整类名

acl_object_identity 表:

列名 类型 描述
id BIGINT 主键,自增
object_id_class BIGINT 外键,关联 acl_class 表的 id,表示对象类型
object_id_identity VARCHAR(36) 领域对象的唯一标识符 (UUID),需要转成字符串
parent_object BIGINT 外键,指向父对象的 id (如果是层次结构的 ACL)
entries_inheriting BOOLEAN 是否继承父对象的权限
acl_sid BIGINT 外键,关联 acl_sid 表的 id,表示 ACL 的所有者
owner_sid BIGINT 外键,关联 acl_sid 表的 id, 标识对象的所有者(可选,可能跟acl_sid相同)

acl_entry 表:

列名 类型 描述
id BIGINT 主键,自增
acl_object_identity BIGINT 外键,关联 acl_object_identity 表的 id,表示 ACE 所属的对象
ace_order INTEGER ACE 的顺序,决定了权限评估的优先级
sid BIGINT 外键,关联 acl_sid 表的 id,表示 ACE 授予权限的安全主体
mask INTEGER 权限掩码,表示授予的权限,使用位运算表示,例如 1 表示 READ,2 表示 WRITE,4 表示 CREATE,8 表示 DELETE 等。
granting BOOLEAN 是否授予权限 (true) 或拒绝权限 (false)
audit_success BOOLEAN 是否记录成功的访问尝试
audit_failure BOOLEAN 是否记录失败的访问尝试

4. Spring Security ACL 的配置

要使用 Spring Security ACL,需要进行以下配置:

  1. 添加依赖:pom.xml 文件中添加 Spring Security ACL 的依赖。
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-acl</artifactId>
</dependency>
  1. 配置数据源: 配置 Spring Security ACL 使用的数据源。通常,这与应用程序的数据源相同。
@Configuration
public class AclConfiguration {

    @Bean
    public DataSource dataSource() {
        // 配置数据源,例如使用 HikariCP
        HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver");
        hikariConfig.setJdbcUrl("jdbc:mysql://localhost:3306/acl_db?serverTimezone=UTC");
        hikariConfig.setUsername("root");
        hikariConfig.setPassword("password");
        return new HikariDataSource(hikariConfig);
    }
}
  1. 创建 ACL Schema: 创建 ACL 相关的数据库表。Spring Security 提供了 SQL 脚本,可以在 org.springframework.security.acls 包下找到,例如 schema.sql (通用) 或 schema-mysql.sql (MySQL)。 根据所使用的数据库类型选择相应的脚本并执行。

  2. 配置 AclService: 配置 AclService bean。通常使用 JdbcMutableAclService

@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class AclConfiguration {

    @Autowired
    private DataSource dataSource;

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

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

    @Bean
    public AclCache aclCache() {
        return new SpringCacheBasedAclCache(cache(), permissionGrantingStrategy(), aclAuthorizationStrategy(new SimpleSecurityExpressionHandler()));
    }

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

    @Bean
    public AclAuthorizationStrategy aclAuthorizationStrategy(SecurityExpressionHandler securityExpressionHandler) {
        return new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ROLE_ADMINISTRATOR"), new SimpleGrantedAuthority("ROLE_ADMINISTRATOR"), new SimpleGrantedAuthority("ROLE_ADMINISTRATOR"));
    }

    @Bean
    public Cache cache() {
        return new ConcurrentMapCache("aclCache");
    }

    @Bean
    public MethodSecurityExpressionHandler expressionHandler() {
        return new DefaultMethodSecurityExpressionHandler();
    }

}
  1. 启用方法级别的安全性: 使用 @EnableMethodSecurity 注解启用方法级别的安全性。prePostEnabled = true 允许使用 @PreAuthorize@PostAuthorize 注解。

5. 使用 Spring Security ACL

  1. 获取 AclService: 在需要使用 ACL 的地方注入 AclService
@Service
public class DocumentService {

    @Autowired
    private AclService aclService;

    // ...
}
  1. 创建 ObjectIdentity: 使用 ObjectIdentity 标识领域对象。
import org.springframework.security.acls.domain.ObjectIdentityImpl;
import org.springframework.security.acls.model.ObjectIdentity;

@Service
public class DocumentService {

    @Autowired
    private AclService aclService;

    public void createDocument(Document document) {
        // ... 保存 document 到数据库 ...

        ObjectIdentity objectIdentity = new ObjectIdentityImpl(Document.class, document.getId());
        // ...
    }
}
  1. 创建 Sid: 使用 PrincipalSidGrantedAuthoritySid 标识安全主体。
import org.springframework.security.acls.domain.PrincipalSid;
import org.springframework.security.acls.model.Sid;
import org.springframework.security.core.context.SecurityContextHolder;

@Service
public class DocumentService {

    @Autowired
    private AclService aclService;

    public void createDocument(Document document) {
        // ... 保存 document 到数据库 ...

        ObjectIdentity objectIdentity = new ObjectIdentityImpl(Document.class, document.getId());
        String username = SecurityContextHolder.getContext().getAuthentication().getName();
        Sid owner = new PrincipalSid(username);
        // ...
    }
}
  1. 创建 Acl: 使用 AclService 创建 Acl
import org.springframework.security.acls.domain.ObjectIdentityImpl;
import org.springframework.security.acls.domain.PrincipalSid;
import org.springframework.security.acls.model.*;

@Service
public class DocumentService {

    @Autowired
    private AclService aclService;

    public void createDocument(Document document) {
        // ... 保存 document 到数据库 ...

        ObjectIdentity objectIdentity = new ObjectIdentityImpl(Document.class, document.getId());
        String username = SecurityContextHolder.getContext().getAuthentication().getName();
        Sid owner = new PrincipalSid(username);

        MutableAcl acl = null;
        try {
            acl = ((MutableAclService) aclService).createAcl(objectIdentity);
        } catch (NotFoundException e) {
            acl = ((MutableAclService) aclService).createAcl(objectIdentity);
        }
        acl.setOwner(owner);
        acl.insertAce(acl.getEntries().size(), BasePermission.ADMINISTRATION, owner, true);
        acl.insertAce(acl.getEntries().size(), BasePermission.READ, owner, true);
        acl.insertAce(acl.getEntries().size(), BasePermission.WRITE, owner, true);
        ((MutableAclService) aclService).updateAcl(acl);
    }
}
  1. 添加 Ace: 使用 AclinsertAce 方法添加 Ace
import org.springframework.security.acls.domain.BasePermission;
import org.springframework.security.acls.domain.ObjectIdentityImpl;
import org.springframework.security.acls.domain.PrincipalSid;
import org.springframework.security.acls.model.*;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

@Service
public class DocumentService {

    @Autowired
    private AclService aclService;

    public void createDocument(Document document) {
        // ... 保存 document 到数据库 ...

        ObjectIdentity objectIdentity = new ObjectIdentityImpl(Document.class, document.getId());
        String username = SecurityContextHolder.getContext().getAuthentication().getName();
        Sid owner = new PrincipalSid(username);

        MutableAcl acl = null;
        try {
            acl = ((MutableAclService) aclService).createAcl(objectIdentity);
        } catch (NotFoundException e) {
            acl = ((MutableAclService) aclService).createAcl(objectIdentity);
        }
        acl.setOwner(owner);

        // 授予所有者 READ, WRITE, ADMIN 权限
        acl.insertAce(acl.getEntries().size(), BasePermission.READ, owner, true);
        acl.insertAce(acl.getEntries().size(), BasePermission.WRITE, owner, true);
        acl.insertAce(acl.getEntries().size(), BasePermission.ADMINISTRATION, owner, true);

        ((MutableAclService) aclService).updateAcl(acl);
    }
}
  1. 使用 @PreAuthorize@PostAuthorize 进行权限检查: 在方法上使用 @PreAuthorize@PostAuthorize 注解,使用 Spring EL (Expression Language) 表达式进行权限检查。
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service
public class DocumentService {

    @Autowired
    private AclService aclService;

    @PreAuthorize("hasPermission(#document, 'READ')")
    public Document getDocument(Document document) {
        // ... 获取 document ...
        return document;
    }

    @PreAuthorize("hasPermission(#document, 'WRITE')")
    public void updateDocument(Document document) {
        // ... 更新 document ...
    }

    @PreAuthorize("hasPermission(#document, 'DELETE')")
    public void deleteDocument(Document document) {
        // ... 删除 document ...
    }
}

@PreAuthorize 注解中,hasPermission(#document, 'READ') 表示当前用户是否拥有对 document 对象的 READ 权限。Spring Security ACL 会自动根据 ACL 信息进行权限检查。

6. 示例代码:文档管理系统

假设我们有一个文档管理系统,用户可以创建、读取、更新和删除文档。我们可以使用 Spring Security ACL 来实现细粒度的权限控制。

Document 类:

import java.util.UUID;

public class Document {

    private UUID id;
    private String title;
    private String content;

    public Document() {
        this.id = UUID.randomUUID();
    }

    public UUID getId() {
        return id;
    }

    public void setId(UUID id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

DocumentService 类:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.acls.domain.BasePermission;
import org.springframework.security.acls.domain.ObjectIdentityImpl;
import org.springframework.security.acls.domain.PrincipalSid;
import org.springframework.security.acls.model.*;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.UUID;

@Service
public class DocumentService {

    @Autowired
    private AclService aclService;

    // 模拟数据库存储
    private DocumentRepository documentRepository = new DocumentRepository();

    public Document createDocument(Document document) {
        // ... 保存 document 到数据库 ...
        documentRepository.save(document);

        ObjectIdentity objectIdentity = new ObjectIdentityImpl(Document.class, document.getId());
        String username = SecurityContextHolder.getContext().getAuthentication().getName();
        Sid owner = new PrincipalSid(username);

        MutableAcl acl = null;
        try {
            acl = ((MutableAclService) aclService).createAcl(objectIdentity);
        } catch (NotFoundException e) {
            acl = ((MutableAclService) aclService).createAcl(objectIdentity);
        }

        acl.setOwner(owner);

        // 授予所有者 READ, WRITE, ADMIN 权限
        acl.insertAce(acl.getEntries().size(), BasePermission.READ, owner, true);
        acl.insertAce(acl.getEntries().size(), BasePermission.WRITE, owner, true);
        acl.insertAce(acl.getEntries().size(), BasePermission.ADMINISTRATION, owner, true);

        ((MutableAclService) aclService).updateAcl(acl);

        return document;
    }

    @PreAuthorize("hasPermission(#id, 'com.example.Document', 'READ')")
    public Document getDocument(UUID id) {
        // ... 获取 document ...
        return documentRepository.findById(id);
    }

    @PreAuthorize("hasPermission(#document, 'WRITE')")
    public void updateDocument(Document document) {
        // ... 更新 document ...
        documentRepository.update(document);
    }

    @PreAuthorize("hasPermission(#id, 'com.example.Document', 'DELETE')")
    public void deleteDocument(UUID id) {
        // ... 删除 document ...
        documentRepository.delete(id);

        ObjectIdentity objectIdentity = new ObjectIdentityImpl(Document.class, id);
        try {
            ((MutableAclService) aclService).deleteAcl(objectIdentity, true);
        } catch (NotFoundException e) {
            // Object identity not found in database; silently ignore
        }
    }

    public void grantPermission(UUID documentId, String username, Permission permission) {
        ObjectIdentity objectIdentity = new ObjectIdentityImpl(Document.class, documentId);
        Sid recipient = new PrincipalSid(username);

        MutableAcl acl = (MutableAcl) aclService.readAclById(objectIdentity);
        acl.insertAce(acl.getEntries().size(), permission, recipient, true);
        aclService.updateAcl(acl);
    }
}

// 模拟数据库
class DocumentRepository {
    private static final java.util.Map<UUID, Document> documents = new java.util.HashMap<>();

    public void save(Document document) {
        documents.put(document.getId(), document);
    }

    public Document findById(UUID id) {
        return documents.get(id);
    }

    public void update(Document document) {
        documents.put(document.getId(), document);
    }

    public void delete(UUID id) {
        documents.remove(id);
    }
}

Controller 类:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.UUID;

@RestController
@RequestMapping("/documents")
public class DocumentController {

    @Autowired
    private DocumentService documentService;

    @PostMapping
    public ResponseEntity<Document> createDocument(@RequestBody Document document) {
        Document createdDocument = documentService.createDocument(document);
        return new ResponseEntity<>(createdDocument, HttpStatus.CREATED);
    }

    @GetMapping("/{id}")
    public ResponseEntity<Document> getDocument(@PathVariable UUID id) {
        Document document = documentService.getDocument(id);
        if (document != null) {
            return new ResponseEntity<>(document, HttpStatus.OK);
        } else {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }

    @PutMapping
    public ResponseEntity<Void> updateDocument(@RequestBody Document document) {
        documentService.updateDocument(document);
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteDocument(@PathVariable UUID id) {
        documentService.deleteDocument(id);
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }

    @PostMapping("/{documentId}/permissions/{username}/{permission}")
    public ResponseEntity<Void> grantPermission(
            @PathVariable UUID documentId,
            @PathVariable String username,
            @PathVariable String permission) {

        try {
            documentService.grantPermission(documentId, username, BasePermission.valueOf(permission.toUpperCase()));
            return new ResponseEntity<>(HttpStatus.OK);
        } catch (IllegalArgumentException e) {
            return new ResponseEntity<>(HttpStatus.BAD_REQUEST); // Invalid permission
        }
    }
}

在这个例子中,我们使用了 @PreAuthorize 注解来保护 getDocumentupdateDocumentdeleteDocument 方法。只有拥有相应权限的用户才能访问这些方法。createDocument 方法在创建文档后,会为当前用户授予所有权限。

Security 配置:

需要在 Spring Security 配置中配置 MethodSecurityExpressionHandler

@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    @Autowired
    private AclConfiguration aclConfiguration;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authz -> authz
                        .requestMatchers("/documents/**").authenticated()
                        .anyRequest().permitAll()
                )
                .httpBasic(withDefaults())
                .csrf(csrf -> csrf.disable());
        return http.build();
    }

    @Bean
    public MethodSecurityExpressionHandler expressionHandler() {
        return new DefaultMethodSecurityExpressionHandler();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("user1").password("{noop}password").roles("USER").build());
        manager.createUser(User.withUsername("user2").password("{noop}password").roles("USER").build());
        return manager;
    }
}

7. 层次结构的 ACL

Spring Security ACL 支持层次结构的 ACL,允许从父对象继承权限。例如,如果一个文件夹拥有一个 ACL,那么该文件夹下的所有文件都可以继承该 ACL 的权限。

要实现层次结构的 ACL,需要在 acl_object_identity 表中设置 parent_object 列,指向父对象的 ID。 同时设置entries_inheriting列为true,表示是否继承。

8. 优化 ACL 性能

ACL 的性能可能会成为一个问题,特别是当 ACL 的数量很大时。以下是一些优化 ACL 性能的建议:

  • 使用缓存: Spring Security ACL 提供了 AclCache 接口,可以使用缓存来存储 ACL 信息,避免频繁访问数据库。
  • 使用批量操作: JdbcMutableAclService 提供了批量操作的方法,例如 readAclsById,可以一次性读取多个 ACL 信息。
  • 避免过度授权: 只授予用户需要的最小权限,避免过度授权。
  • 合理设计 ACL 结构: 合理设计 ACL 结构,避免创建过多的 ACL。如果多个对象具有相同的权限,可以考虑使用一个 ACL 并将其应用于多个对象。

9. 注意事项

  • 数据库事务: 在修改 ACL 信息时,需要确保在数据库事务中进行,以保证数据的一致性。
  • 安全性: 确保 ACL 信息的安全性,防止未经授权的访问和修改。
  • 复杂性: ACL 的配置和管理可能会比较复杂,需要仔细设计和测试。

10. 总结

Spring Security ACL 是一种强大的工具,用于实现细粒度的权限控制。 通过配置AclService,创建ObjectIdentity、Sid和Permission, 可以针对单个领域对象实例设置权限,从而实现更精细、更灵活的权限管理。 合理使用 ACL 可以提高应用程序的安全性,并满足复杂的权限需求。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注