On this page
27. 域对象安全性(ACL)
27.1 Overview
复杂的应用程序经常会发现需要定义访问权限,而不仅仅是在 Web 请求或方法调用级别。取而代之的是,安全决策需要同时包括谁(Authentication
),何处(MethodInvocation
)和什么(SomeDomainObject
)。换句话说,授权决策还需要考虑方法调用的实际域对象实例主题。
假设您正在设计宠物诊所的应用程序。基于 Spring 的应用程序将有两个主要的用户组:宠物诊所的工作人员以及宠物诊所的 Client。工作人员将有权访问所有数据,而您的 Client 只能看到他们自己的 Client 记录。为了使其更有趣一点,您的 Client 可以允许其他用户查看其 Client 记录,例如其“幼稚园幼教”导师或本地“小马俱乐部”总裁。以 Spring Security 为基础,您可以使用几种方法:
编写您的业务方法以增强安全性。您可以在
Customer
域对象实例中查询集合,以确定哪些用户有权访问。通过使用SecurityContextHolder.getContext().getAuthentication()
,您将可以访问Authentication
对象。编写
AccessDecisionVoter
以增强Authentication
对象中存储的GrantedAuthority[]
的安全性。这意味着您的AuthenticationManager
需要用代表用户可以访问的Customer
域对象实例的自定义GrantedAuthority[]
填充Authentication
。编写
AccessDecisionVoter
以增强安全性,然后直接打开目标Customer
域对象。这意味着您的投票者需要访问 DAO,以使其能够检索Customer
对象。然后它将访问Customer
对象的已批准用户的集合并做出适当的决定。
这些方法中的每一种都是完全合法的。但是,第一个将您的授权检查与您的业务代码结合在一起。这样做的主要问题包括单元测试的难度增加以及在其他地方重用Customer
授权逻辑会更加困难。从Authentication
对象获得GrantedAuthority[]
s 也是可以的,但不会扩展为大量的Customer
s。如果用户可能能够访问 5,000_s(在这种情况下不太可能,但是可以想象如果它是大型 Pony Club 的流行兽医!),那么消耗内存和构造Authentication
对象所需的时间将是不希望的。最后一种方法,直接从外部代码打开Customer
,可能是这三种方法中最好的。它可以实现关注点分离,并且不会滥用内存或 CPU 周期,但是它仍然效率低下,因为AccessDecisionVoter
和最终的业务方法本身都将执行对负责检索Customer
对象的 DAO 的调用。每个方法调用两次访问显然是不可取的。此外,列出每种方法时,您都需要从头开始编写自己的访问控制列表(ACL)持久性和业务逻辑。
幸运的是,还有另一种选择,我们将在下面讨论。
27.2 关键概念
Spring Security 的 ACL 服务位于spring-security-acl-xxx.jar
中。您将需要将此 JAR 添加到 Classpath 中,以使用 Spring Security 的域对象实例安全性功能。
Spring Security 的域对象实例安全性功能以访问控制列表(ACL)的概念为中心。系统中的每个域对象实例都有其自己的 ACL,并且 ACL 记录了谁可以使用该域对象以及不能使用该域对象的详细信息。考虑到这一点,Spring Security 为您的应用程序提供了三个与 ACL 相关的主要功能:
一种有效检索所有域对象的 ACL 条目(并修改这些 ACL)的方法
确保在调用方法之前允许给定的主体使用对象的方法
在调用方法之后,一种确保给定的委托人可以使用您的对象(或它们返回的对象)的方法
如第一个项目要点所示,Spring Security ACL 模块的主要功能之一是提供一种高性能的 ACL 检索方法。这个 ACL Repositories 功能非常重要,因为系统中的每个域对象实例都可能有多个访问控制项,并且每个 ACL 都可能以树状结构从其他 ACL 继承(Spring 对此提供了开箱即用的支持)安全性,并且非常常用)。 Spring Security 的 ACL 功能经过精心设计,可提供高性能的 ACL 检索,以及可插入的缓存,最小化死锁的数据库更新,与 ORM 框架的独立性(我们直接使用 JDBC),适当的封装以及透明的数据库更新。
给定数据库对于 ACL 模块的操作至关重要,让我们研究实现中默认使用的四个主表。以下是典型的 Spring Security ACL 部署中按大小 Sequences 显示的表,最后列出的行数最多:
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)和 Management(位 4)。如果您希望使用其他权限,则很容易实现自己的Permission
实例,并且 ACL 框架的其余部分将在不了解扩展的情况下运行。
重要的是要了解,系统中域对象的数量与我们选择使用整数位掩码这一事实完全无关。虽然您有 32 位可用的权限,但您可能有数十亿个域对象实例(这意味着 ACL_OBJECT_IDENTITY 中的数十亿行,很可能是 ACL_ENTRY)。之所以说出这一点,是因为我们发现有时人们会误以为每个潜在的域对象都需要一点,事实并非如此。
现在,我们已经提供了有关 ACL 系统功能以及它在表结构中的外观的基本概述,让我们探索关键接口。关键接口是:
Acl
:每个域对象只有一个Acl
对象,该对象在内部保存AccessControlEntry
,并且知道Acl
的所有者。 Acl 不直接引用域对象,而是ObjectIdentity
。Acl
存储在 ACL_OBJECT_IDENTITY 表中。AccessControlEntry
:Acl
包含多个AccessControlEntry
,在框架中通常缩写为 ACE。每个 ACE 都引用Permission
,Sid
和Acl
的特定 Tuples。 ACE 也可以是授予或不授予的,并且包含审核设置。 ACE 存储在 ACL_ENTRY 表中。Permission
:权限代表特定的不可变位掩码,并提供用于位掩码和输出信息的便捷功能。上面显示的基本权限(位 0 至 4)包含在BasePermission
类中。Sid
:ACL 模块需要引用主体和GrantedAuthority[]
。Sid
接口提供了间接级别,它是“安全身份”的缩写。常见的类包括PrincipalSid
(以表示Authentication
对象内的主体)和GrantedAuthoritySid
。安全标识信息存储在 ACL_SID 表中。ObjectIdentity
:每个域对象在 ACL 模块内部由ObjectIdentity
表示。默认实现称为ObjectIdentityImpl
。AclService
:检索适用于给定ObjectIdentity
的Acl
。在包含的实现(JdbcAclService
)中,检索操作委托给LookupStrategy
。LookupStrategy
提供了一种高度优化的策略,用于使用批量检索((BasicLookupStrategy
)来检索 ACL 信息,并支持利用实例化视图,分层查询和类似的以性能为中心的非 ANSI SQL 功能的自定义实现。MutableAclService
:允许显示经过修改的Acl
以保持持久性。如果您不愿意,则不必使用此界面。
请注意,我们现成的 AclService 和相关数据库类均使用 ANSI SQL。因此,这应该适用于所有主要数据库。在撰写本文时,已使用 Hypersonic SQL,PostgreSQL,Microsoft SQL Server 和 Oracle 成功测试了该系统。
Spring Security 附带了两个示例,它们演示了 ACL 模块。第一个是联系人 samples,另一个是文档 Management 系统(DMS)samples。我们建议您看一下这些示例。
27.3 入门
要开始使用 Spring Security 的 ACL 功能,您需要将 ACL 信息存储在某个地方。这需要使用 Spring 实例化DataSource
。然后将DataSource
注入到JdbcMutableAclService
和BasicLookupStrategy
实例中。后者提供高性能的 ACL 检索功能,而前者提供了 mutator 功能。有关示例配置,请参阅 Spring Security 附带的示例之一。您还需要使用上一节中列出的四个特定于 ACL 的表填充数据库(有关适当的 SQL 语句,请参阅 ACL 示例)。
创建所需的架构并实例化JdbcMutableAclService
之后,接下来需要确保您的域模型支持与 Spring Security ACL 软件包的互操作性。希望ObjectIdentityImpl
足够了,因为它提供了多种使用方式。大多数人将拥有包含public Serializable getId()
方法的域对象。如果返回类型为 long 或与 long 兼容(例如 int),您将发现无需进一步考虑ObjectIdentity
问题。 ACL 模块的许 Multipart 都依赖长标识符。如果您不使用 long(或 int,byte 等),则很有可能需要重新实现许多类。我们不打算在 Spring Security 的 ACL 模块中支持非长标识符,因为长已经与所有数据库序列(最常见的标识符数据类型)兼容,并且长度足以容纳所有常见的使用场景。
以下代码片段显示了如何创建Acl
或修改现有的Acl
:
// 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);
在上面的示例中,我们检索与标识号为 44 的“ Foo”域对象相关联的 ACL。然后,我们添加 ACE,以便名为“ Samantha”的主体可以“Management”该对象。除了 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 信息,从而使您无需编写任何代码。请参考示例应用程序以学习如何使用这些类。