域对象安全 (ACLs)

本节介绍 Spring Security 如何使用访问控制列表 (ACL) 提供域对象安全。

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

假设您正在为宠物诊所设计应用程序。您的基于 Spring 的应用程序有两个主要用户组:宠物诊所的工作人员和宠物诊所的客户。工作人员应该可以访问所有数据,而客户只能查看自己的客户记录。为了使它更有趣,您的客户可以允许其他用户查看他们的客户记录,例如他们的“幼犬学前班”导师或当地“Pony Club”的主席。当您使用 Spring Security 作为基础时,您有几种可能的方法

  • 编写您的业务方法以强制执行安全。您可以查询 Customer 域对象实例中的集合以确定哪些用户有权访问。通过使用 SecurityContextHolder.getContext().getAuthentication(),您可以访问 Authentication 对象。

  • 编写一个 AccessDecisionVoter 以从存储在 Authentication 对象中的 GrantedAuthority[] 实例中强制执行安全。这意味着您的 AuthenticationManager 需要使用自定义 GrantedAuthority[] 对象填充 Authentication,以表示主体有权访问的每个 Customer 域对象实例。

  • 编写一个AccessDecisionVoter来强制执行安全并直接打开目标Customer域对象。这意味着您的投票者需要访问一个 DAO,该 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)的方法

  • 一种在调用方法之前确保给定主体被允许使用您的对象的方法

  • 一种在调用方法之后确保给定主体被允许使用您的对象(或它们返回的内容)的方法

如第一个要点所述,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 不直接引用域对象,而是引用 ObjectIdentityAcl 存储在 ACL_OBJECT_IDENTITY 表中。

  • AccessControlEntry:一个 Acl 持有多个 AccessControlEntry 对象,在框架中通常简称为 ACE。每个 ACE 引用 PermissionSidAcl 的特定元组。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:检索适用于给定 ObjectIdentityAcl。在包含的实现(JdbcAclService)中,检索操作委托给 LookupStrategyLookupStrategy 提供了一种高度优化的策略来检索 ACL 信息,使用批量检索(BasicLookupStrategy)并支持使用物化视图、分层查询以及类似以性能为中心的非 ANSI SQL 功能的自定义实现。

  • MutableAclService:允许将修改后的 Acl 提交到持久化。使用此接口是可选的。

请注意,我们的 AclService 和相关的数据库类都使用 ANSI SQL。因此,这应该适用于所有主要数据库。在撰写本文时,该系统已成功在 Hypersonic SQL、PostgreSQL、Microsoft SQL Server 和 Oracle 上测试。

Spring Security 附带两个演示 ACL 模块的示例。第一个是 Contacts Sample,另一个是 Document Management System (DMS) Sample。我们建议您查看这些示例。

入门

要开始使用 Spring Security 的 ACL 功能,您需要将 ACL 信息存储在某个地方。这需要在 Spring 中实例化一个 DataSource。然后将 DataSource 注入到 JdbcMutableAclServiceBasicLookupStrategy 实例中。前者提供变异器功能,后者提供高性能的 ACL 检索功能。有关示例配置,请参阅随 Spring Security 提供的 示例 之一。您还需要使用上一节中列出的 四个特定于 ACL 的表 来填充数据库(有关相应的 SQL 语句,请参阅 ACL 示例)。

创建完所需的模式并实例化 JdbcMutableAclService 后,您需要确保您的域模型支持与 Spring Security ACL 包的互操作性。希望 ObjectIdentityImpl 足够用,因为它提供了许多使用方式。大多数人的域对象都包含一个 public Serializable getId() 方法。如果返回类型是 long 或与 long 兼容(例如 int),您可能会发现您无需进一步考虑 ObjectIdentity 问题。ACL 模块的许多部分都依赖于长标识符。如果您不使用 long(或 intbyte 等),您可能需要重新实现多个类。我们不打算在 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 信息自动集成到您的服务层操作中。我们发现这种方法很有效。

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

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