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,需要进行以下配置:
- 添加依赖: 在
pom.xml
文件中添加 Spring Security ACL 的依赖。
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-acl</artifactId>
</dependency>
- 配置数据源: 配置 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);
}
}
-
创建 ACL Schema: 创建 ACL 相关的数据库表。Spring Security 提供了 SQL 脚本,可以在
org.springframework.security.acls
包下找到,例如schema.sql
(通用) 或schema-mysql.sql
(MySQL)。 根据所使用的数据库类型选择相应的脚本并执行。 -
配置 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();
}
}
- 启用方法级别的安全性: 使用
@EnableMethodSecurity
注解启用方法级别的安全性。prePostEnabled = true
允许使用@PreAuthorize
和@PostAuthorize
注解。
5. 使用 Spring Security ACL
- 获取 AclService: 在需要使用 ACL 的地方注入
AclService
。
@Service
public class DocumentService {
@Autowired
private AclService aclService;
// ...
}
- 创建 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());
// ...
}
}
- 创建 Sid: 使用
PrincipalSid
或GrantedAuthoritySid
标识安全主体。
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);
// ...
}
}
- 创建 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);
}
}
- 添加 Ace: 使用
Acl
的insertAce
方法添加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);
}
}
- 使用
@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
注解来保护 getDocument
、updateDocument
和 deleteDocument
方法。只有拥有相应权限的用户才能访问这些方法。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 可以提高应用程序的安全性,并满足复杂的权限需求。