域对象(Domain Object)安全(ACL)

本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。

本节介绍Spring Security如何通过访问控制列表(Access Control List - ACL)提供域对象安全。

复杂的应用程序通常需要定义超出web请求或方法调用级别的访问权限。相反,安全决策需要包括谁(Authentication)、哪里(MethodInvocation)、以及什么(SomeDomainObject)。换句话说,授权决策也需要考虑方法调用的实际域(domain)对象实例主体。

想象一下,你正在为一家宠物诊所设计一个应用程序。基于Spring的应用程序有两个主要的用户群:宠物诊所的员工和宠物诊所的客户。工作人员应该可以访问所有的数据,而你的客户应该只能看到自己的客户记录。为了使它更有趣,你的客户可以让其他用户看到他们的客户记录,如他们的 “puppy preschool” 导师或当地 “Pony Club” 的主席。当你使用Spring Security作为基础时,你有几种可能的方法。

  • 编写你的业务方法来执行security。你可以查阅客户域对象实例中的一个集合,以确定哪些用户有访问权。通过使用 SecurityContextHolder.getContext().getAuthentication(),你可以访问 Authentication 对象。

  • 编写一个 AccessDecisionVoter 来执行存储在 Authentication 对象中的 GrantedAuthority[] 实例的安全性。这意味着你的 AuthenticationManager 需要用自定义的 GrantedAuthority[] 对象来填充 Authentication,以代表委托人可以访问的每个客户域对象实例。

  • 写一个 AccessDecisionVoter 来执行security,直接打开目标 Customer 域对象。这就意味着你的投票者(voter)需要访问一个DAO,让它检索 Customer 对象。然后,它可以访问 Customer 对象的批准用户集合并做出适当的决定。

这些方法中的每一种都是完全合法的。然而,第一种方法是将你的授权检查与你的业务代码结合起来。这样做的主要问题包括单元测试的难度增加,以及在其他地方重用 Customer 授权逻辑会更加困难。从 Authentication 对象中获取 GrantedAuthority[] 实例也不错,但不能扩展到大量的 Customer 对象。如果一个用户可以访问5000个 Customer 对象(在这种情况下不太可能,但想象一下,如果它是一个大型小马俱乐部的流行兽医!),所消耗的内存量和构建认证对象所需的时间将是不可取的。最后一种方法是直接从外部代码中打开 Customer,这可能是三种方法中最好的。它实现了关注点的分离,没有滥用内存或CPU周期,但它仍然是低效的,因为 AccessDecisionVoter 和最终的业务方法本身都对负责检索 Customer 对象的DAO进行了调用。每个方法调用两次访问显然是不可取的。此外,对于列出的每一种方法,你都需要从头开始编写自己的访问控制列表(ACL)持久化和业务逻辑。

幸运的是,还有另一种选择,我们在后面讨论。

关键概念

Spring Security的ACL服务是在 spring-security-acl-xxx.jar 中提供的。你需要将这个JAR添加到你的classpath中,以使用Spring Security的域对象实例安全功能。

Spring Security的域对象实例安全功能以访问控制列表(ACL)的概念为中心。你系统中的每个域对象实例都有自己的ACL,ACL记录了谁可以和谁不能使用该域对象的细节。考虑到这一点,Spring Security为你的应用程序提供了三个主要的ACL相关功能。

  • 一种有效检索所有域对象的ACL条目的方法(以及修改这些ACL)。

  • 在方法被调用之前,确保某个委托人(principal)被允许使用你的对象的一种方法。

  • 一种确保特定委托人(principal)在方法被调用后被允许处理你的对象(或它们返回的东西)的方式。

正如第一个要点所示,Spring Security ACL 模块的主要功能之一是提供一种检索ACL的高性能方式。这种ACL库的能力是非常重要的,因为你系统中的每个域对象实例可能有几个访问控制条目,而且每个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权限的每个独特的类,都有一个单行记录。

  • ACL_OBJECT_IDENTITY 存储系统中每个独特的域对象实例的信息。列包括ID,ACL_CLASS 表的外键,一个唯一的标识符,因此我们知道我们提供信息的ACL_CLASS实例,父级,ACL_SID表的外键,代表域对象实例的所有者,以及我们是否允许ACL条目继承任何父ACL。我们为每个存储ACL权限的域对象实例都有一条记录。

  • 最后,ACL_ENTRY 存储分配给每个接收者的个人权限。列包括 ACL_OBJECT_IDENTITY 的外键,recipient(即ACL_SID的外键),我们是否要进行审计,以及代表实际被授予或拒绝的权限的integer bit 掩码。我们为每一个收到域对象工作权限的 recipient 准备了一条记录。

如上一段所述,ACL系统使用bit掩码。然而,你不需要知道bit掩码的细节之处来使用ACL系统。只要说我们有32个位可以打开或关闭就够了。这些位中的每一个都代表一个权限。默认情况下,权限是读(bit 0),写(bit 1),创建(bit 2),删除(bit 3),和管理(bit 4)。如果你想使用其他权限,你可以实现你自己的 Permission 实例,ACL框架的其余部分在不知道你的扩展的情况下运行。

你应该明白,你系统中的域对象的数量与我们选择使用int bit 掩码的事实完全没有关系。虽然你有32位可用的权限,但你可能有数十亿的域对象实例(这意味着在 ACL_OBJECT_IDENTITY 和可能的 ACL_ENTRY 中有数十亿的行)。我们提出这一点是因为我们发现人们有时会错误地认为他们需要为每个潜在的域对象提供一个bit,但事实并非如此。

现在我们已经提供了一个关于ACL系统的基本概述,以及它在表结构层面上的样子,我们需要探索关键的接口。

  • Acl: 每个域对象都有一个且只有一个 Acl 对象,它在内部持有 AccessControlEntry 对象,并知道 Acl 的所有者。Acl 并不直接指向域对象,而是指向一个 ObjectIdentityAcl 存储在 ACL_OBJECT_IDENTITY 表中。

  • AccessControlEntry: 一个 Acl 持有多个 AccessControlEntry 对象,这些对象在框架中通常被缩写为ACE。每个ACE指的是 PermissionSidAcl 的一个特定元组(Tuple)。一个 ACE 也可以是授予或不授予的,并包含审计设置。ACE 被存储在 ACL_ENTRY 表中。

  • Permission: 一个权限代表一个特定的不可变的bit掩码,并为bit掩码和输出信息提供方便的功能。上面介绍的基本权限(bit 0到4)包含在 BasePermission 类中。

  • Sid: ACL模块需要引用委托人(principal)和 GrantedAuthority[] 实例。Sid 接口提供了一个层次的间接性。(“SID`"是 "`Security IDentity” 的缩写)常见的类包括 PrincipalSid(代表 Authentication 对象中的 principal)和 GrantedAuthoritySid。安全身份信息被存储在 ACL_SID 表中。

  • ObjectIdentity: 每个领域对象在 ACL 模块内部由一个 ObjectIdentity 表示。默认的实现被称为 ObjectIdentityImpl

  • AclService: 检索适用于给定 ObjectIdentityACL。在包含的实现(JdbcAclService)中,检索操作被委托给一个 LookupStrategyLookupStrategy 为检索ACL信息提供了高度优化的策略,使用分批检索(BasicLookupStrategy)并支持使用物化视图、分层查询和类似的以性能为中心的非ANSI SQL功能的定制实现。

  • MutableAclService: 让一个修改过的 Acl 呈现出来,以便持久化。这个接口的使用是可选的。

注意,我们的 AclService 和相关的数据库类都使用ANSI SQL。因此,这应该能与所有主要的数据库一起工作。在撰写本文时,该系统已经成功地与Hypersonic SQL、PostgreSQL、Microsoft SQL Server和Oracle进行了测试。

Spring Security提供的两个样本展示了ACL模块。第一个是 Contact 示例,另一个是 文件管理系统(DMS)示例。我们建议看一下这些例子。

入门

要开始使用Spring Security的ACL功能,你需要将ACL信息存储在某个地方。这就需要在Spring中实例化一个 DataSource。然后,DataSource 被注入到 JdbcMutableAclServiceBasicLookupStrategy 实例中。前者提供变种功能,后者提供高性能的ACL检索功能。参见Spring Security附带的 示例 中的一个配置示例。你还需要用上一节中列出的 四个ACL专用表 来填充数据库(参见ACL示例中的相应SQL语句)。

一旦你创建了所需的schema并实例化了 JdbcMutableAclService,你需要确保你的领域模型支持与Spring Security ACL包的互操作性。希望 ObjectIdentityImpl 证明是足够的,因为它提供了大量可以使用的方式。大多数人的领域对象都包含一个 public Serializable getId() 方法。如果返回类型是 long 或与长 long 兼容(如 int),你可能会发现你不需要进一步考虑 ObjectIdentity 问题。ACL模块的许多部分依赖于 long 标识符。如果你不使用 long(或 intbyte 等等),你可能需要重新实现一些类。我们不打算在Spring Security的ACL模块中支持非 long 标识符,因为 long 标识符已经与所有数据库序列兼容,是最常见的标识符数据类型,而且长度足以满足所有常见的使用场景。

下面的代码片段显示了如何创建一个 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” 的委托人就可以 “administer” 这个对象。除了 insertAce 方法外,这段代码是不言而喻的。insertAce 方法的第一个参数决定在Acl中插入新条目的位置。在前面的例子中,我们把新的ACE放在现有ACE的末尾。最后一个参数是一个布尔值,表示该ACE是授予还是拒绝。大多数时候,它授予(true)。但是,如果它拒绝(false),权限就会被有效地阻止。

Spring Security 没有提供任何特殊的集成来自动创建、更新或删除ACL作为DAO或存储库操作的一部分。相反,你需要为你的个人领域对象编写类似于前面例子中的代码。你应该考虑在你的服务层上使用AOP,将ACL信息与你的服务层操作自动集成。我们已经发现这种方法很有效。

一旦你使用这里描述的技术在数据库中存储了一些ACL信息,下一步就是实际使用ACL信息作为授权决策逻辑的一部分。在这里你有很多选择。你可以编写你自己的 AccessDecisionVoterAfterInvocationProvider,(分别)在方法调用之前或之后启动。这样的类将使用 AclService 来检索相关的ACL,然后调用 Acl.isGranted(Permission[] permission, Sid[] sids, boolean administrativeMode) 来决定权限是被授予还是拒绝。另外,你可以使用我们的 AclEntryVoterAclEntryAfterInvocationProviderAclEntryAfterInvocationCollectionFilteringProvider 类。所有这些类都提供了一种基于声明的方法来在运行时评估ACL信息,使你无需编写任何代码。

请参阅 [示例应用程序,了解如何使用这些类。