Domain Object Security (ACLs)

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

This section describes how Spring Security provides domain object security with Access Control Lists (ACLs).

复杂的应用程序通常需要定义高于 Web 请求或方法调用级别的访问权限。 相反,安全决策需要包括人员 (Authentication)、位置 (MethodInvocation)和事项 (SomeDomainObject)。 换句话说,授权决策还需要考虑方法调用的实际领域对象实例主体。

Complex applications often need to define access permissions beyond a web request or method invocation level. Instead, security decisions need to comprise who (Authentication), where (MethodInvocation), and what (SomeDomainObject). In other words, authorization decisions also need to consider the actual domain object instance subject of a method invocation.

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

Imagine you are designing an application for a pet clinic. There are two main groups of users of your Spring-based application: staff of the pet clinic and the pet clinic’s customers. The staff should have access to all of the data, while your customers should be able to see only their own customer records. To make it a little more interesting, your customers can let other users see their customer records, such as their “puppy preschool” mentor or the president of their local “Pony Club”. When you use Spring Security as the foundation, you have several possible approaches:

  • Write your business methods to enforce the security. You could consult a collection within the Customer domain object instance to determine which users have access. By using SecurityContextHolder.getContext().getAuthentication(), you can access the Authentication object.

  • Write an AccessDecisionVoter to enforce the security from the GrantedAuthority[] instances stored in the Authentication object. This means that your AuthenticationManager needs to populate the Authentication with custom GrantedAuthority[] objects to represent each of the Customer domain object instances to which the principal has access.

  • Write an AccessDecisionVoter to enforce the security and open the target Customer domain object directly. This would mean your voter needs access to a DAO that lets it retrieve the Customer object. It can then access the Customer object’s collection of approved users and make the appropriate decision.

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

Each one of these approaches is perfectly legitimate. However, the first couples your authorization checking to your business code. The main problems with this include the enhanced difficulty of unit testing and the fact that it would be more difficult to reuse the Customer authorization logic elsewhere. Obtaining the GrantedAuthority[] instances from the Authentication object is also fine but will not scale to large numbers of Customer objects. If a user can access 5,000 Customer objects (unlikely in this case, but imagine if it were a popular vet for a large Pony Club!) the amount of memory consumed and the time required to construct the Authentication object would be undesirable. The final method, opening the Customer directly from external code, is probably the best of the three. It achieves separation of concerns and does not misuse memory or CPU cycles, but it is still inefficient in that both the AccessDecisionVoter and the eventual business method itself perform a call to the DAO responsible for retrieving the Customer object. Two accesses per method invocation is clearly undesirable. In addition, with every approach listed, you need to write your own access control list (ACL) persistence and business logic from scratch.

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

Fortunately, there is another alternative, which we discuss later.

Key Concepts

Spring Security 的 ACL 服务在 spring-security-acl-xxx.jar 中发货。 您需要将此 JAR 添加到您的类路径中才能使用 Spring Security 的领域对象实例安全功能。

Spring Security’s ACL services are shipped in the spring-security-acl-xxx.jar. You need to add this JAR to your classpath to use Spring Security’s domain object instance security capabilities.

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

Spring Security’s domain object instance security capabilities center on the concept of an access control list (ACL). Every domain object instance in your system has its own ACL, and the ACL records details of who can and cannot work with that domain object. With this in mind, Spring Security provides three main ACL-related capabilities to your application:

  • A way to efficiently retrieve ACL entries for all of your domain objects (and modifying those ACLs)

  • A way to ensure a given principal is permitted to work with your objects before methods are called

  • A way to ensure a given principal is permitted to work with your objects (or something they return) after methods are called

正如第一个要点所指出的,Spring Security ACL 模块的主要功能之一是提供一种高性能的方式来检索 ACL。 此 ACL 存储库功能非常重要,因为您系统中的每个领域对象实例可能有多个访问控制条目,并且每个 ACL 可能从树状结构中的其他 ACL 中继承(Spring Security 支持这一点,并且非常常用)。 Spring Security 的 ACL 功能经过精心设计,可提供高性能的 ACL 检索,以及可插拔的缓存、最小化死锁的数据库更新、独立于 ORM 框架(我们直接使用 JDBC)、适当的封装和透明的数据库更新。

As indicated by the first bullet point, one of the main capabilities of the Spring Security ACL module is providing a high-performance way of retrieving ACLs. This ACL repository capability is extremely important, because every domain object instance in your system might have several access control entries, and each ACL might inherit from other ACLs in a tree-like structure (this is supported by Spring Security, and it is very commonly used). Spring Security’s ACL capability has been carefully designed to provide high performance retrieval of ACLs, together with pluggable caching, deadlock-minimizing database updates, independence from ORM frameworks (we use JDBC directly), proper encapsulation, and transparent database updating.

鉴于数据库对于 ACL 模块的操作至关重要,我们需要探索实现中默认使用的四个主要表。 表格按典型 Spring Security ACL 部署中大小的顺序排列,最后列出具有最多行的表格:

Given that databases are central to the operation of the ACL module, we need explore the four main tables used by default in the implementation. The tables are presented in order of size in a typical Spring Security ACL deployment, with the table with the most rows listed last:

  • ACL_SID lets us uniquely identify any principal or authority in the system (“SID” stands for “Security IDentity”). The only columns are the ID, a textual representation of the SID, and a flag to indicate whether the textual representation refers to a principal name or a GrantedAuthority. Thus, there is a single row for each unique principal or GrantedAuthority. When used in the context of receiving a permission, an SID is generally called a “recipient”.

  • ACL_CLASS lets us uniquely identify any domain object class in the system. The only columns are the ID and the Java class name. Thus, there is a single row for each unique Class for which we wish to store ACL permissions.

  • ACL_OBJECT_IDENTITY stores information for each unique domain object instance in the system. Columns include the ID, a foreign key to the ACL_CLASS table, a unique identifier so we know the ACL_CLASS instance for which we provide information, the parent, a foreign key to the ACL_SID table to represent the owner of the domain object instance, and whether we allow ACL entries to inherit from any parent ACL. We have a single row for every domain object instance for which we store ACL permissions.

  • Finally, ACL_ENTRY stores the individual permissions assigned to each recipient. Columns include a foreign key to the ACL_OBJECT_IDENTITY, the recipient (i.e. a foreign key to ACL_SID), whether we’ll be auditing or not, and the integer bit mask that represents the actual permission being granted or denied. We have a single row for every recipient that receives a permission to work with a domain object.

如上一段所述,ACL 系统使用整数位掩码。 但是,您不必了解位移的更精细要点即可使用 ACL 系统。 只要说我们可以切换32位就足够了。 每个位都表示一个权限。 默认情况下,权限为读取(位 0)、写入(位 1)、创建(位 2)、删除(位 3)和管理(位 4)。 您可以实现自己的`Permission`实例,如果您希望使用其他权限,ACL 框架的其余部分将独立于您的扩展而运行。

As mentioned in the last paragraph, the ACL system uses integer bit masking. However, you need not be aware of the finer points of bit shifting to use the ACL system. Suffice it to say that we have 32 bits we can switch on or off. Each of these bits represents a permission. By default, the permissions are read (bit 0), write (bit 1), create (bit 2), delete (bit 3), and administer (bit 4). You can implement your own Permission instance if you wish to use other permissions, and the remainder of the ACL framework operates without knowledge of your extensions.

您应该理解,系统中领域对象的数量与我们选择使用整数位掩码这件事绝对无关。 虽然您有 32 位可用作权限,但您可以拥有数十亿个领域对象实例(这意味着 ACL_OBJECT_IDENTITY 中有数十亿行,可能还有 ACL_ENTRY)。 我们提出这一点,因为我们发现人们有时错误地认为他们需要为每个潜在的领域对象一个位,但事实并非如此。

You should understand that the number of domain objects in your system has absolutely no bearing on the fact that we have chosen to use integer bit masking. While you have 32 bits available for permissions, you could have billions of domain object instances (which means billions of rows in ACL_OBJECT_IDENTITY and, probably, ACL_ENTRY). We make this point because we have found that people sometimes mistakenly that believe they need a bit for each potential domain object, which is not the case.

既然我们已经基本了解了 ACL 系统的作用以及它在表结构级别上的样子,我们需要探索关键接口:

Now that we have provided a basic overview of what the ACL system does, and what it looks like at a table-structure level, we need to explore the key interfaces:

  • Acl: Every domain object has one and only one Acl object, which internally holds the AccessControlEntry objects and knows the owner of the Acl. An Acl does not refer directly to the domain object, but instead to an ObjectIdentity. The Acl is stored in the ACL_OBJECT_IDENTITY table.

  • AccessControlEntry: An Acl holds multiple AccessControlEntry objects, which are often abbreviated as ACEs in the framework. Each ACE refers to a specific tuple of Permission, Sid, and Acl. An ACE can also be granting or non-granting and contain audit settings. The ACE is stored in the ACL_ENTRY table.

  • Permission: A permission represents a particular immutable bit mask and offers convenience functions for bit masking and outputting information. The basic permissions presented above (bits 0 through 4) are contained in the BasePermission class.

  • Sid: The ACL module needs to refer to principals and GrantedAuthority[] instances. A level of indirection is provided by the Sid interface. (“SID” is an abbreviation of “Security IDentity”.) Common classes include PrincipalSid (to represent the principal inside an Authentication object) and GrantedAuthoritySid. The security identity information is stored in the ACL_SID table.

  • ObjectIdentity: Each domain object is represented internally within the ACL module by an ObjectIdentity. The default implementation is called ObjectIdentityImpl.

  • AclService: Retrieves the Acl applicable for a given ObjectIdentity. In the included implementation (JdbcAclService), retrieval operations are delegated to a LookupStrategy. The LookupStrategy provides a highly optimized strategy for retrieving ACL information, using batched retrievals (BasicLookupStrategy) and supporting custom implementations that use materialized views, hierarchical queries, and similar performance-centric, non-ANSI SQL capabilities.

  • MutableAclService: Lets a modified Acl be presented for persistence. Use of this interface is optional.

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

Note that our AclService and related database classes all use ANSI SQL. This should therefore work with all major databases. At the time of writing, the system had been successfully tested with Hypersonic SQL, PostgreSQL, Microsoft SQL Server, and Oracle.

Spring Security 附带两个样本,演示了 ACL 模块。第一个是 联系信息示例,另一个是 文档管理系统 (DMS) 示例。我们建议查看这些示例。

Two samples ship with Spring Security that demonstrate the ACL module. The first is the Contacts Sample, and the other is the Document Management System (DMS) Sample. We suggest taking a look at these examples.

Getting Started

要开始使用 Spring Security 的 ACL 功能,您需要将 ACL 信息存储在某个地方。这需要在 Spring 中实例化 DataSource。然后将 `DataSource`注入到 `JdbcMutableAclService`和 `BasicLookupStrategy`实例中。前者提供修改器功能,而后者提供高性能 ACL 检索功能。请参阅随 Spring Security 一起提供的某个 示例,了解示例配置。您还需要使用上一节中列出的 four ACL-specific tables填充数据库(有关适当的 SQL 语句,请参阅 ACL 示例)。

To get starting with Spring Security’s ACL capability, you need to store your ACL information somewhere. This necessitates the instantiation of a DataSource in Spring. The DataSource is then injected into a JdbcMutableAclService and a BasicLookupStrategy instance. The former provides mutator capabilities, and the latter provides high-performance ACL retrieval capabilities. See one of the samples that ship with Spring Security for an example configuration. You also need to populate the database with the acl_tables listed in the previous section (see the ACL samples for the appropriate SQL statements).

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

Once you have created the required schema and instantiated JdbcMutableAclService, you need to ensure your domain model supports interoperability with the Spring Security ACL package. Hopefully, ObjectIdentityImpl proves sufficient, as it provides a large number of ways in which it can be used. Most people have domain objects that contain a public Serializable getId() method. If the return type is long or compatible with long (such as an int), you may find that you need not give further consideration to ObjectIdentity issues. Many parts of the ACL module rely on long identifiers. If you do not use long (or an int, byte, and so on), you probably need to reimplement a number of classes. We do not intend to support non-long identifiers in Spring Security’s ACL module, as longs are already compatible with all database sequences, are the most common identifier data type, and are of sufficient length to accommodate all common usage scenarios.

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

The following fragment of code shows how to create an Acl or modify an existing 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),则权限将被有效阻止。

In the preceding example, we retrieve the ACL associated with the Foo domain object with identifier number 44. We then add an ACE so that a principal named “Samantha” can “administer” the object. The code fragment is relatively self-explanatory, except for the insertAce method. The first argument to the insertAce method determine position in the Acl at which the new entry is inserted. In the preceding example, we put the new ACE at the end of the existing ACEs. The final argument is a Boolean indicating whether the ACE is granting or denying. Most of the time it grants (true). However, if it denies (false), the permissions are effectively being blocked.

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

Spring Security does not provide any special integration to automatically create, update, or delete ACLs as part of your DAO or repository operations. Instead, you need to write code similar to that shown in the preceding example for your individual domain objects. You should consider using AOP on your services layer to automatically integrate the ACL information with your services layer operations. We have found this approach to be effective.

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

Once you have used the techniques described here to store some ACL information in the database, the next step is to actually use the ACL information as part of authorization decision logic. You have a number of choices here. You could write your own AccessDecisionVoter or AfterInvocationProvider that (respectively) fires before or after a method invocation. Such classes would use AclService to retrieve the relevant ACL and then call Acl.isGranted(Permission[] permission, Sid[] sids, boolean administrativeMode) to decide whether permission is granted or denied. Alternately, you could use our AclEntryVoter, AclEntryAfterInvocationProvider or AclEntryAfterInvocationCollectionFilteringProvider classes. All of these classes provide a declarative-based approach to evaluating ACL information at runtime, freeing you from needing to write any code.

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

See the sample applications to learn how to use these classes.