跳到主要内容

领域对象安全(ACLs)

QWen Max 中英对照 Domain Object Security ACLs Domain Object Security (ACLs)

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

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

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

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

  • 编写一个 AccessDecisionVoter 来从 Authentication 对象中存储的 GrantedAuthority[] 实例强制执行安全性。这意味着你的 AuthenticationManager 需要用自定义的 GrantedAuthority[] 对象填充 Authentication,以表示主体可以访问的每个 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 被允许操作你的对象的方法

  • 一种在调用方法之后确保给定 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 通常被称为“接收者”。

  • 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 对象,在框架中通常缩写为 ACEs。每个 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:检索给定 ObjectIdentity 适用的 Acl。在包含的实现(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 提供的 samples 之一。你还需要用上一节列出的 四个特定于 ACL 的表 来填充数据库(请参阅 ACL 示例以获取适当的 SQL 语句)。

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

以下代码片段展示了如何创建或修改现有的 Acl

// 代码示例
java
// 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);
java

在前面的示例中,我们检索与标识符为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信息,使你无需编写任何代码。

参见sample applications以了解如何使用这些类。