域对象安全 (ACL)

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

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

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

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

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

  • 编写一个 AccessDecisionVoter 以实施安全并直接打开目标 Customer 域对象。这意味着您的投票者需要访问一个 DAO,该 DAO 允许它检索 Customer 对象。然后,它可以访问 Customer 对象的已批准用户集合并做出适当的决定。

这些方法都完全合理。但是,第一种方法将授权检查与业务代码耦合在一起。这带来的主要问题包括单元测试难度增加,以及Customer授权逻辑难以在其他地方重用。从Authentication对象获取GrantedAuthority[]实例也很好,但无法扩展到大量Customer对象。如果一个用户可以访问5000个Customer对象(在这种情况下不太可能,但想象一下,如果它是一个大型Pony Club的热门兽医!),那么消耗的内存量和构建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”是“安全标识”的缩写。)常用类包括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 模块的示例。第一个是联系人示例,另一个是文档管理系统 (DMS) 示例。我们建议您查看这些示例。

入门

要开始使用 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 信息用作授权决策逻辑的一部分。您在这里有多种选择。您可以编写自己的AccessDecisionVoterAfterInvocationProvider,它们分别在方法调用之前或之后触发。此类类将使用AclService检索相关的 ACL,然后调用Acl.isGranted(Permission[] permission, Sid[] sids, boolean administrativeMode)来决定是否授予或拒绝权限。或者,您可以使用我们的AclEntryVoterAclEntryAfterInvocationProviderAclEntryAfterInvocationCollectionFilteringProvider类。所有这些类都提供了一种基于声明的方法来在运行时评估 ACL 信息,使您无需编写任何代码。

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