# ACLS

## 概述

复杂的应用程序通常会发现需要定义访问权限，而不仅仅是在 Web 请求或方法调用级别。相反，安全决策需要包括 who（`Authentication`）、where（`MethodInvocation`）和 what（`SomeDomainObject`）。换句话说，授权决策还需要考虑方法调用的实际领域对象实例主题。

想象一下，你正在为一家宠物诊所设计一个应用程序。基于 Spring 的应用程序主要有两类用户：宠物诊所的工作人员，以及宠物诊所的客户。员工可以访问所有的数据，而你的客户只能看到他们自己的客户记录。为了让它更有趣，你的客户可以允许其他用户查看他们的客户记录，例如他们的“Puppy Prementary”导师或当地“Pony Club”的总裁。使用 Spring 安全性作为基础，你可以使用几种方法：

* 编写你的业务方法以加强安全性。你可以查阅`Customer`域对象实例中的集合，以确定哪些用户具有访问权限。通过使用`SecurityContextHolder.getContext().getAuthentication()`，你将能够访问`Authentication`对象。

* 编写`AccessDecisionVoter`以从存储在`Authentication`对象中的`GrantedAuthority[]`s 强制执行安全性。这将意味着你的`AuthenticationManager`将需要用自定义的`GrantedAuthority[]`填充`Authentication`，表示主体可以访问的每个`Customer`域对象实例。

* 编写`AccessDecisionVoter`以强制执行安全性，并直接打开目标`Customer`域对象。这意味着你的投票者需要访问允许它检索`Customer`对象的 DAO。然后，它将访问`Customer`对象的已批准用户集合，并做出适当的决定。

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

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

## 关键概念

Spring Security 的 ACL 服务以`spring-security-acl-xxx.jar`发送。你需要将此 JAR 添加到 Classpath 中，以使用 Spring Security 的域对象实例安全功能。

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

* 一种有效地检索所有域对象的 ACL 条目的方法（并修改这些 ACLS）

* 在调用方法之前，一种确保给定的主体可以与对象一起工作的方法

* 在调用方法之后，一种确保给定的主体允许与你的对象（或它们返回的东西）一起工作的方法

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

给定数据库是 ACL 模块操作的核心，让我们研究一下实现中默认使用的四个主表。下面按照典型的 Spring 安全性 ACL 部署中的大小顺序列出了这些表，最后列出了行最多的表：

* ACL\_SID 允许我们唯一地标识系统中的任何主体或权限（“SID”代表“安全标识”）。唯一的列是 ID、SID 的文本表示以及一个标志，用于指示文本表示是指主体名称还是`GrantedAuthority`。因此，对于每个唯一的主体有一个单行或`GrantedAuthority`。当用于接收许可的上下文时，SID 通常称为“收件人”。

* ACL\_CLASS 允许我们唯一地标识系统中的任意域对象类。唯一的列是 ID 和 爪哇 类名。因此，对于我们希望为其存储 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`s，并且知道`Acl`的所有者。ACL 不直接指向域对象，而是指`ObjectIdentity`。`Acl`存储在 ACL\_Object\_Identity 表中。

* `AccessControlEntry`:一个`Acl`包含多个`AccessControlEntry`s，这些在框架中通常被缩写为 ACES。每个 ACE 指的是`Permission`，`Sid`和`Acl`的特定元组。ACE 还可以是授予或不授予，并包含审计设置。ACE 存储在 ACL\_entry 表中。

* `Permission`:权限表示特定的不可变位掩码，并为位掩码和输出信息提供方便的功能。上面提供的基本权限（从 0 位到 4 位）包含在`BasePermission`类中。

* `Sid`:ACL 模块需要引用主体和`GrantedAuthority[]`s。间接级别由`Sid`接口提供，它是“安全标识”的缩写。常见的类包括`PrincipalSid`（表示`Authentication`对象内的主体）和`GrantedAuthoritySid`。安全标识信息存储在 ACL\_SID 表中。

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

* `AclService`:检索适用于给定`ObjectIdentity`的`Acl`。在包含的实现中（`JdbcAclService`），检索操作被委托给`LookupStrategy`。`LookupStrategy`为检索 ACL 信息提供了一种高度优化的策略，它使用批检索（`BasicLookupStrategy`）和支持定制实现，这些实现利用物化视图、分层查询和类似的以性能为中心的非 ANSI SQL 功能。

* `MutableAclService`:允许为持久性而呈现修改后的`Acl`。如果你不想使用此接口，那么使用此接口并不是必需的。

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

两个示例附带 Spring 安全性，演示了 ACL 模块。第一个是[触点样品](https://github.com/spring-projects/spring-security-samples/tree/5.6.x/servlet/xml/java/contacts)，另一个是[文档管理系统示例](https://github.com/spring-projects/spring-security-samples/tree/5.6.x/servlet/xml/java/dms)。我们建议看看这些例子。

## 开始

要开始使用 Spring Security 的 ACL 功能，你需要将 ACL 信息存储在某个地方。这需要使用 Spring 实例化`DataSource`。然后将`DataSource`注入到`JdbcMutableAclService`和`BasicLookupStrategy`实例中。后者提供高性能的 ACL 检索功能，前者提供 Mutator 功能。对于示例配置，请参考带有 Spring 安全性的示例之一。你还需要使用上一节中列出的四个特定于 ACL 的表填充数据库（对于相应的 SQL 语句，请参考 ACL 示例）。

一旦你创建了所需的模式并实例化了`JdbcMutableAclService`，接下来就需要确保你的域模型支持与 Spring Security ACL 包的互操作性。希望`ObjectIdentityImpl`将被证明是足够的，因为它提供了大量可以使用它的方法。大多数人都会拥有包含`public Serializable getId()`方法的域对象。如果返回类型是 long，或者与 long 兼容（例如，INT），你将发现不需要进一步考虑`ObjectIdentity`问题。ACL 模块的许多部分依赖于长标识符。如果你不使用 long（或 INT、字节等），那么你很有可能需要重新实现一些类。我们不打算在 Spring Security 的 ACL 模块中支持非长标识符，因为 Longs 已经与所有数据库序列（最常见的标识符数据类型）兼容，并且具有足够的长度来适应所有常见的使用场景。

下面的代码片段展示了如何创建`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);
```

Kotlin

```
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 放在现有 ACES 的末尾。最后一个论点是一个布尔值，它表明 ACE 是授予还是否认。在大多数情况下，它将授予（真），但如果它拒绝（假），则权限将被有效地阻止。

Spring 安全性不提供任何特殊的集成以自动创建、更新或删除 ACLS 作为 DAO 或存储库操作的一部分。相反，你将需要为你的各个域对象编写上面所示的代码。值得考虑在服务层使用 AOP 来自动将 ACL 信息与服务层操作集成在一起。在过去，我们发现这是一种非常有效的方法。

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

[方法安全性](method-security.html)[OAuth2](../oauth2/index.html)

