领域对象安全(ACLs)
本节介绍 Spring Security 如何通过访问控制列表(ACLs)提供领域对象安全。
复杂的应用程序通常需要定义超出Web请求或方法调用级别的访问权限。相反,安全决策需要包含谁(Authentication)、在哪里(MethodInvocation)以及什么(SomeDomainObject)。换句话说,授权决策还需要考虑方法调用的实际域对象实例主体。
假设你正在为一家宠物诊所设计一个应用程序。你的基于Spring的应用程序有两类主要用户:宠物诊所的员工和宠物诊所的客户。员工应能访问所有数据,而客户应只能查看他们自己的客户记录。为了让事情更有趣一点,你的客户可以允许其他用户查看他们的客户记录,例如他们的“幼犬学前班”导师或当地“小马俱乐部”的主席。当你使用Spring Security作为基础时,你有几种可能的方法:
-
编写业务方法以实施安全控制。你可以通过查询
Customer领域对象实例内的集合来确定哪些用户拥有访问权限。通过使用SecurityContextHolder.getContext().getAuthentication(),你可以访问Authentication对象。 -
编写一个
AuthorizationManager,基于存储在Authentication对象中的GrantedAuthority[]实例来实施安全控制。这意味着你的AuthenticationManager需要用自定义的GrantedAuthority[]对象来填充Authentication,以代表主体有权访问的每一个Customer领域对象实例。 -
编写一个
AuthorizationManager来实施安全控制,并直接打开目标Customer领域对象。这意味着你的投票器需要访问一个 DAO,以便它能检索Customer对象。然后,它可以访问Customer对象的已授权用户集合并做出相应的决策。
上述每种方法都完全合理。然而,第一种方法将授权检查与业务代码耦合在一起。其主要问题包括单元测试难度增加,以及 Customer 授权逻辑在其他地方更难复用。从 Authentication 对象获取 GrantedAuthority[] 实例也是可行的,但无法扩展到大量 Customer 对象的情况。如果一个用户可以访问 5,000 个 Customer 对象(在本例中不太可能,但想象一下如果它是一个大型小马俱乐部中受欢迎的兽医!),那么消耗的内存量和构建 Authentication 对象所需的时间将是不可取的。最后一种方法,直接从外部代码打开 Customer,可能是三者中最好的。它实现了关注点分离,并且没有滥用内存或 CPU 周期,但效率仍然不高,因为 AuthorizationManager 和最终的业务方法本身都会调用负责检索 Customer 对象的 DAO。每次方法调用进行两次访问显然是不可取的。此外,对于列出的每种方法,您都需要从头开始编写自己的访问控制列表(ACL)持久化和业务逻辑。
幸运的是,还有另一种选择,我们稍后会讨论。
关键概念
Spring Security 的 ACL 服务包含在 spring-security-acl-xxx.jar 中。您需要将此 JAR 文件添加到类路径中,才能使用 Spring Security 的领域对象实例安全功能。
如果你需要使用包含 AclEntryVoter 的旧版 Access API,请同时引入 spring-security-access-xxx.jar。
Spring Security的领域对象实例安全功能围绕访问控制列表(ACL)的概念展开。系统中的每个领域对象实例都拥有自己的ACL,该列表详细记录了谁可以或不可以操作该领域对象。基于此,Spring Security为应用程序提供了三项主要的ACL相关功能:
-
一种高效检索所有领域对象的ACL条目(并修改这些ACL)的方法
-
一种确保在方法调用前,给定主体被允许操作您的对象的方法
-
一种确保在方法调用后,给定主体被允许操作您的对象(或它们返回的内容)的方法
正如第一点所述,Spring Security ACL模块的主要能力之一是提供高性能的ACL检索方式。这种ACL存储能力至关重要,因为系统中的每个领域对象实例都可能包含多个访问控制条目,且每个ACL可能以树状结构继承其他ACL(Spring Security支持此功能且被广泛使用)。Spring Security的ACL功能经过精心设计,在提供高性能ACL检索的同时,还具备可插拔缓存、最小化死锁的数据库更新、独立于ORM框架(我们直接使用JDBC)、适当的封装以及透明的数据库更新等特性。
鉴于数据库是ACL模块运行的核心,我们需要探讨实现中默认使用的四个主要表。这些表格按照典型Spring Security ACL部署中的数据量大小排序,行数最多的表列在最后:
-
ACL_SID表用于唯一标识系统中的任何主体或权限(“SID”代表“安全身份”)。其列仅包括ID、SID的文本表示形式,以及一个标志位,用于指示该文本表示形式是指向主体名称还是GrantedAuthority。因此,每个唯一的主体或GrantedAuthority都对应一行记录。在接收权限的上下文中使用时,SID通常被称为“接收者”。 -
ACL_CLASS表用于唯一标识系统中的任何域对象类。其列仅包括ID和Java类名。因此,每个我们希望存储ACL权限的唯一类都对应一行记录。 -
ACL_OBJECT_IDENTITY表存储系统中每个唯一域对象实例的信息。其列包括ID、指向ACL_CLASS表的外键、一个唯一标识符(以便我们知道提供信息的ACL_CLASS实例)、父级信息、指向ACL_SID表的外键(用于表示域对象实例的所有者),以及是否允许ACL条目从任何父级ACL继承。对于我们存储ACL权限的每个域对象实例,都有一行对应的记录。 -
最后,
ACL_ENTRY表存储分配给每个接收者的单个权限。其列包括指向ACL_OBJECT_IDENTITY的外键、接收者(即指向ACL_SID的外键)、是否进行审计,以及表示实际授予或拒绝权限的整数位掩码。对于每个获得域对象操作权限的接收者,都有一行对应的记录。
正如上一段所述,ACL系统采用整数位掩码机制。不过,使用ACL系统时无需深入了解位移操作的细节。简而言之,我们拥有32个可开关的位,每个位代表一项权限。默认权限包括:读取(位0)、写入(位1)、创建(位2)、删除(位3)和管理(位4)。如需使用其他权限,您可以自行实现 Permission 实例,而ACL框架的其余部分将无需感知您的扩展即可正常运行。
你应该明白,系统中领域对象的数量与我们选择使用整数位掩码完全没有关系。虽然你只有32位可用于权限设置,但你可以拥有数十亿个领域对象实例(这意味着ACL_OBJECT_IDENTITY表以及可能ACL_ENTRY表会有数十亿行)。我们特别强调这一点,是因为我们发现人们有时会错误地认为每个潜在的领域对象都需要一个位,但事实并非如此。
在介绍了ACL系统的基本功能及其表结构概览后,接下来我们需要探讨其关键接口:
-
Acl:每个领域对象有且仅有一个Acl对象,其内部持有AccessControlEntry对象,并知晓该Acl的所有者。Acl 不直接引用领域对象,而是引用一个ObjectIdentity。Acl 存储在ACL_OBJECT_IDENTITY表中。 -
AccessControlEntry:一个Acl持有多个AccessControlEntry对象,在框架中常缩写为 ACE。每个 ACE 引用一个特定的Permission、Sid和Acl三元组。ACE 可以是授予或非授予的,并可包含审计设置。ACE 存储在ACL_ENTRY表中。 -
Permission:权限表示一个特定的不可变位掩码,并提供用于位掩码操作和输出信息的便捷函数。上述基本权限(位 0 至位 4)包含在BasePermission类中。 -
Sid:ACL 模块需要引用主体和GrantedAuthority[]实例。Sid接口提供了一个间接层。(“SID” 是 “Security IDentity” 的缩写。)常见类包括PrincipalSid(用于表示Authentication对象内的主体)和GrantedAuthoritySid。安全身份信息存储在ACL_SID表中。 -
ObjectIdentity:在 ACL 模块内部,每个领域对象由一个ObjectIdentity表示。默认实现称为ObjectIdentityImpl。 -
AclService:检索适用于给定ObjectIdentity的Acl。在包含的实现(JdbcAclService)中,检索操作委托给LookupStrategy。LookupStrategy提供了一种高度优化的 ACL 信息检索策略,它使用批量检索(BasicLookupStrategy),并支持使用物化视图、分层查询以及类似的以性能为中心的非 ANSI SQL 功能的自定义实现。 -
MutableAclService:允许将修改后的Acl提交以进行持久化。此接口的使用是可选的。
请注意,我们的 AclService 及相关数据库类均采用 ANSI SQL 标准。因此,该系统应兼容所有主流数据库。截至撰写本文时,该系统已成功通过 Hypersonic SQL、PostgreSQL、Microsoft SQL Server 和 Oracle 的测试。
Spring Security 随附了两个演示 ACL 模块的示例。第一个是 联系人示例,另一个是 文档管理系统 (DMS) 示例。我们建议查看这些示例。
快速开始
要开始使用 Spring Security 的 ACL 功能,你需要将 ACL 信息存储到某个地方。这需要在 Spring 中实例化一个 DataSource。然后将 DataSource 注入到 JdbcMutableAclService 和 BasicLookupStrategy 实例中。前者提供了修改功能,后者提供了高性能的 ACL 检索功能。有关示例配置,请参阅 Spring Security 附带的某个示例。你还需要用上一节列出的四个 ACL 专用表来填充数据库(请参阅 ACL 示例以获取相应的 SQL 语句)。
一旦你创建了所需的模式并实例化了 JdbcMutableAclService,你需要确保你的领域模型支持与 Spring Security ACL 包的互操作性。希望 ObjectIdentityImpl 能够满足需求,因为它提供了大量可用的方式。大多数人的领域对象都包含一个 public Serializable getId() 方法。如果返回类型是 long 或与 long 兼容(例如 int),你可能会发现无需进一步考虑 ObjectIdentity 问题。ACL 模块的许多部分都依赖于长整型标识符。如果你不使用 long(或 int、byte 等),你可能需要重新实现一些类。我们不打算在 Spring Security 的 ACL 模块中支持非长整型标识符,因为长整型已经与所有数据库序列兼容,是最常见的标识符数据类型,并且长度足以适应所有常见的使用场景。
以下代码片段展示了如何创建一个 Acl 或修改现有的 Acl:
- Java
- Kotlin
// Prepare the information we'd like in our access control entry (ACE)
ObjectIdentity oi = new ObjectIdentityImpl(Foo.class, new Long(44));
Sid sid = new PrincipalSid("Samantha");
Permission p = BasePermission.ADMINISTRATION;
// Create or update the relevant ACL
MutableAcl acl = null;
try {
acl = (MutableAcl) aclService.readAclById(oi);
} catch (NotFoundException nfe) {
acl = aclService.createAcl(oi);
}
// Now grant some permissions via an access control entry (ACE)
acl.insertAce(acl.getEntries().length, p, sid, true);
aclService.updateAcl(acl);
val oi: ObjectIdentity = ObjectIdentityImpl(Foo::class.java, 44)
val sid: Sid = PrincipalSid("Samantha")
val p: Permission = BasePermission.ADMINISTRATION
// Create or update the relevant ACL
var acl: MutableAcl? = null
acl = try {
aclService.readAclById(oi) as MutableAcl
} catch (nfe: NotFoundException) {
aclService.createAcl(oi)
}
// Now grant some permissions via an access control entry (ACE)
acl!!.insertAce(acl.entries.size, p, sid, true)
aclService.updateAcl(acl)
在前面的示例中,我们获取了与标识符为44的Foo域对象关联的ACL。然后我们添加了一个ACE,以便名为“Samantha”的主体可以“管理”该对象。这段代码片段相对容易理解,除了insertAce方法。insertAce方法的第一个参数决定了新条目在ACL中的插入位置。在前面的示例中,我们将新的ACE放在了现有ACE的末尾。最后一个参数是一个布尔值,表示该ACE是授予权限还是拒绝权限。大多数情况下是授予(true)。然而,如果是拒绝(false),则权限实际上会被阻止。
Spring Security 并未提供任何特殊集成来自动创建、更新或删除 ACL(访问控制列表),作为 DAO 或存储库操作的一部分。相反,您需要为各个领域对象编写类似于前面示例中所示的代码。您应考虑在服务层使用 AOP(面向切面编程),以自动将 ACL 信息与服务层操作集成。我们发现这种方法非常有效。
使用 PermissionEvaluator
一旦你使用这里描述的技术将一些ACL信息存储到数据库中,下一步就是在授权决策逻辑中实际使用这些ACL信息。
在这里你有多种选择,主要的一种是在你的 @PreAuthorize、@PostAuthorize、@PreFilter 和 @PostFilter 注解表达式中使用 AclPermissionEvaluator。
以下是配置 AclPermissionEvaluator 到授权逻辑中所需组件的示例清单:
- Java
- Kotlin
@EnableMethodSecurity
@Configuration
class SecurityConfig {
@Bean
static MethodSecurityExpressionHandler expressionHandler(AclPermissionEvaluator aclPermissionEvaluator) {
final DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(aclPermissionEvaluator);
return expressionHandler;
}
@Bean
static AclPermissionEvaluator aclPermissionEvaluator(AclService aclService) {
return new AclPermissionEvaluator(aclService);
}
@Bean
static JdbcMutableAclService aclService(DataSource dataSource, LookupStrategy lookupStrategy, AclCache aclCache) {
return new JdbcMutableAclService(dataSource, lookupStrategy, aclCache);
}
@Bean
static LookupStrategy lookupStrategy(DataSource dataSource, AclCache cache,
AclAuthorizationStrategy aclAuthorizationStrategy, PermissionGrantingStrategy permissionGrantingStrategy) {
return new BasicLookupStrategy(dataSource, cache, aclAuthorizationStrategy, permissionGrantingStrategy);
}
@Bean
static AclCache aclCache(PermissionGrantingStrategy permissionGrantingStrategy,
AclAuthorizationStrategy aclAuthorizationStrategy) {
Cache cache = new ConcurrentMapCache("aclCache");
return new SpringCacheBasedAclCache(cache, permissionGrantingStrategy, aclAuthorizationStrategy);
}
@Bean
static AclAuthorizationStrategy aclAuthorizationStrategy() {
return new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ADMIN"));
}
@Bean
static PermissionGrantingStrategy permissionGrantingStrategy() {
return new DefaultPermissionGrantingStrategy(new ConsoleAuditLogger());
}
}
@EnableMethodSecurity
@Configuration
internal object SecurityConfig {
@Bean
fun expressionHandler(aclPermissionEvaluator: AclPermissionEvaluator?): MethodSecurityExpressionHandler {
val expressionHandler = DefaultMethodSecurityExpressionHandler()
expressionHandler.setPermissionEvaluator(aclPermissionEvaluator)
return expressionHandler
}
@Bean
fun aclPermissionEvaluator(aclService: AclService?): AclPermissionEvaluator {
return AclPermissionEvaluator(aclService)
}
@Bean
fun aclService(dataSource: DataSource?, lookupStrategy: LookupStrategy?, aclCache: AclCache?): JdbcMutableAclService {
return JdbcMutableAclService(dataSource, lookupStrategy, aclCache)
}
@Bean
fun lookupStrategy(dataSource: DataSource?, cache: AclCache?,
aclAuthorizationStrategy: AclAuthorizationStrategy?, permissionGrantingStrategy: PermissionGrantingStrategy?): LookupStrategy {
return BasicLookupStrategy(dataSource, cache, aclAuthorizationStrategy, permissionGrantingStrategy)
}
@Bean
fun aclCache(permissionGrantingStrategy: PermissionGrantingStrategy?,
aclAuthorizationStrategy: AclAuthorizationStrategy?): AclCache {
val cache: Cache = ConcurrentMapCache("aclCache")
return SpringCacheBasedAclCache(cache, permissionGrantingStrategy, aclAuthorizationStrategy)
}
@Bean
fun aclAuthorizationStrategy(): AclAuthorizationStrategy {
return AclAuthorizationStrategyImpl(SimpleGrantedAuthority("ADMIN"))
}
@Bean
fun permissionGrantingStrategy(): PermissionGrantingStrategy {
return DefaultPermissionGrantingStrategy(ConsoleAuditLogger())
}
}
然后,使用基于方法的安全性,你可以在注解表达式中使用 hasPermission,如下所示:
- Java
- Kotlin
@GetMapping
@PostFilter("hasPermission(filterObject, read)")
Iterable<Message> getAll() {
return this.messagesRepository.findAll();
}
@GetMapping
@PostFilter("hasPermission(filterObject, read)")
fun getAll(): Iterable<Message> {
return this.messagesRepository.findAll()
}