|
此版本仍在开发中,尚未被视为稳定版本。如需最新稳定版本,请使用 Spring Security 7.0.4! |
领域对象安全(ACL)
本节介绍 Spring Security 如何通过访问控制列表(ACL)提供领域对象安全。
复杂的应用程序通常需要定义超出 Web 请求或方法调用级别的访问权限。
相反,安全决策需要综合考虑“谁”(Authentication)、“在哪里”(MethodInvocation)以及“什么”(SomeDomainObject)。
换句话说,授权决策还需要考虑方法调用所针对的实际领域对象实例。
假设你正在设计一个宠物诊所的应用程序。 你的基于 Spring 的应用程序主要有两类用户:宠物诊所的工作人员和宠物诊所的客户。 工作人员应能访问所有数据,而客户只能查看他们自己的客户记录。 为了使情况更有趣一些,客户还可以授权其他用户查看他们的客户记录,例如他们的“幼犬学前班”导师或当地“小马俱乐部”的主席。 当你以 Spring Security 为基础时,有几种可能的实现方式:
-
编写您的业务方法以强制执行安全控制。 您可以查询
Customer领域对象实例中的某个集合,以确定哪些用户拥有访问权限。 通过使用SecurityContextHolder.getContext().getAuthentication(),您可以访问Authentication对象。 -
编写一个
AccessDecisionVoter,以强制执行存储在GrantedAuthority[]对象中的Authentication实例所定义的安全策略。 这意味着您的AuthenticationManager需要用自定义的Authentication对象填充GrantedAuthority[],以表示主体(principal)有权访问的每个Customer领域对象实例。 -
编写一个
AccessDecisionVoter来实施安全控制,并直接打开目标Customer领域对象。 这意味着您的投票器需要访问一个 DAO,以便能够检索Customer对象。 然后,它就可以访问Customer对象中已批准用户的集合,并做出适当的决策。
这些方法中的每一种都是完全合理的。然而,第一种方式将您的授权检查与业务代码耦合在一起。这带来的主要问题包括单元测试的难度增加,以及在其他地方复用Customer授权逻辑变得更加困难。从Authentication对象中获取GrantedAuthority[]的实例也可以,但不会扩展到大量的Customer对象。如果一个用户可以访问 5,000 个Customer对象(这种情况不太可能发生,但假设这是一个受欢迎的兽医,为一个大型小马俱乐部服务!),那么消耗的内存和构建 Authentication 对象所需的时间将变得不切实际。最终方法是从外部代码直接打开Customer,可能是三种方法中最优的。它实现了关注点分离,并且不会滥用内存或 CPU 周期,但它仍然效率低下,因为AccessDecisionVoter和最终的业务方法本身都会调用负责检索Customer对象的 DAO。每次方法调用进行两次访问显然是不可取的。此外,对于所列的每种方法,您都需要从头开始编写自己的访问控制列表(ACL)持久化机制和业务逻辑。
幸运的是,还有另一种替代方案,我们稍后会讨论。
核心概念
Spring Security 的 ACL 服务包含在 spring-security-acl-xxx.jar 中。
您需要将此 JAR 文件添加到类路径中,才能使用 Spring Security 的领域对象实例安全功能。
Spring Security 的领域对象实例安全功能围绕访问控制列表(ACL)这一概念展开。 系统中的每个领域对象实例都有其自己的 ACL,该 ACL 记录了哪些用户可以或不可以操作该领域对象的详细信息。 基于此,Spring Security 为您的应用程序提供了三项主要的 ACL 相关功能:
-
一种高效检索所有领域对象的 ACL 条目(并修改这些 ACL)的方法
-
一种在调用方法之前确保指定主体被允许操作您的对象的方式
-
一种确保在方法调用后,指定的主体(principal)被允许操作您的对象(或其返回的内容)的方式
正如第一个要点所述,Spring Security ACL 模块的主要功能之一是提供一种高性能的方式来检索 ACL(访问控制列表)。 这种 ACL 存储库功能极为重要,因为系统中的每个领域对象实例可能都包含多个访问控制项(ACE),并且每个 ACL 可能以树状结构从其他 ACL 继承(Spring Security 支持此特性,并且在实践中被广泛使用)。 Spring Security 的 ACL 功能经过精心设计,不仅支持高性能的 ACL 检索,还提供了可插拔的缓存机制、最小化死锁的数据库更新、与 ORM 框架的解耦(我们直接使用 JDBC)、良好的封装性,以及透明的数据库更新。
鉴于数据库是ACL模块运行的核心,我们需要探讨默认实现中所使用的四张主要表。 这些表按照在典型的Spring Security ACL部署中数据量的大小顺序列出,行数最多的表排在最后:
-
ACL_SID使我们能够唯一标识系统中的任何主体或权限(“SID”代表“安全标识”,Security IDentity)。 该表仅包含三列:ID、SID 的文本表示形式,以及一个标志位,用于指示该文本表示形式是指主体名称还是GrantedAuthority。 因此,每个唯一的主体或GrantedAuthority在表中都对应一行。 当 SID 用于接收权限的上下文中时,通常被称为“接收者”(recipient)。 -
ACL_CLASS使我们能够在系统中唯一标识任何领域对象类。 该表仅包含 ID 和 Java 类名两列。 因此,对于每个我们希望存储 ACL 权限的唯一 Class,表中都有一行记录。 -
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 模块需要引用主体(principals)和GrantedAuthority[]实例。 通过Sid接口提供了一层间接引用。(“SID” 是 “Security IDentity”(安全标识)的缩写。) 常见的类包括PrincipalSid(用于表示Authentication对象中的主体)和GrantedAuthoritySid。 安全标识信息存储在ACL_SID表中。 -
ObjectIdentity:ACL 模块内部使用ObjectIdentity来表示每个领域对象。 默认实现类为ObjectIdentityImpl。 -
AclService:获取适用于给定Acl的ObjectIdentity。 在所提供的实现(JdbcAclService)中,检索操作被委托给一个LookupStrategy。LookupStrategy提供了一种高度优化的策略来检索 ACL 信息,它使用批量检索(BasicLookupStrategy),并支持使用物化视图、层次查询以及类似的以性能为中心的非 ANSI SQL 功能的自定义实现。 -
MutableAclService:允许将修改后的Acl提交以进行持久化。 使用此接口是可选的。
请注意,我们的 AclService 及相关的数据库类均使用 ANSI SQL。
因此,这应该适用于所有主流数据库。
在撰写本文时,该系统已成功在 Hypersonic SQL、PostgreSQL、Microsoft SQL Server 和 Oracle 上通过测试。
Spring Security 附带了两个示例,用于演示 ACL 模块。 第一个是联系人示例(Contacts Sample),另一个是文档管理系统(DMS)示例(Document Management System (DMS) Sample)。 我们建议您查看这些示例。
快速开始
要开始使用 Spring Security 的 ACL 功能,你需要将 ACL 信息存储在某个地方。
这需要在 Spring 中实例化一个 DataSource。
然后将该 DataSource 注入到 JdbcMutableAclService 和 BasicLookupStrategy 实例中。
前者提供修改(mutator)功能,后者则提供高性能的 ACL 检索功能。
有关示例配置,请参阅 Spring Security 附带的示例之一。
你还需要使用前一节中列出的四个 ACL 专用表来初始化数据库(相关 SQL 语句请参见 ACL 示例)。
一旦您创建了所需的模式并实例化了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 并未提供任何特殊集成来在您的 DAO 或仓库操作中自动创建、更新或删除 ACL。 相反,您需要为各自的领域对象编写类似于前面示例中所示的代码。 您应考虑在服务层使用 AOP,以将 ACL 信息自动集成到您的服务层操作中。 我们发现这种方法非常有效。
使用 PermissionEvaluator
一旦你使用此处描述的技术将一些ACL信息存储到数据库中,下一步就是实际在授权决策逻辑中使用这些ACL信息。
在这里,您有多种选择,其中最主要的是在您的 AclPermissionEvaluator、@PreAuthorize、@PostAuthorize 和 @PreFilter 注解表达式中使用 @PostFilter。
这是将 AclPersmissionEvaluator 集成到您的授权逻辑中所需组件的示例列表:
-
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()
}