章节 VI. 附加主题

在本部分中,我们将介绍一些特性,这些特性需要了解前面的章节以及框架的一些更高级和不太常用的特性。

28. 域对象安全性 (ACL)

28.1 概述

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

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

  • 编写你的业务方法来执行安全性。你可以查阅 Customer 域对象实例中的集合来确定哪些用户可以访问。通过使用 SecurityContextHolder.getContext().getAuthentication(),你将能够访问 Authentication 对象。
  • 编写一个 AccessDecisionVoter,以强制存储在 Authentication 对象中的 GrantedAuthority[] 的安全性。这意味着你的 AuthenticationManager 需要用自定义 GrantedAuthority[] 来填充 Authentication,它代表主体访问的每个 Customer 域对象实例。
  • 编写一个 AccessDecisionVoter 来强制安全,并直接打开目标 Customer 域对象。这意味着你的投票者需要访问一个允许它检索 Customer 对象的 DAO。然后,它将访问 Customer 对象的认可用户集合并做出适当的决定。

这些方法中的每一个都是完全合法的。但是,第一对将你的授权检查与业务代码进行配对。这方面的主要问题包括单元测试的难度增加,以及在别处重用 Customer 授权逻辑将更加困难。从 Authentication 对象获得 GrantedAuthority[] 也是可以的,但是不会扩展到大量的 Customer。如果用户可能能够访问 5,000 个 Customer(在本例中不太可能,但是想象一下它是否是大型 Pony Club 的流行审查!)构建验证对象所需的内存消耗量和时间将是不可取的。最后的方法,直接从外部代码打开 Customer,可能是三个之中最好的。它实现了关注点的分离,并且没有滥用内存或 CPU 周期,但是它仍然效率低下,因为 AccessDecisionVoter 和最终的业务方法本身都将对负责检索 Customer 对象的 DAO 执行调用。每个方法调用的两次访问显然是不可取的。此外,对于列出的每种方法,你都需要从头开始编写自己的访问控制列表(ACL)持久性和业务逻辑。

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

28.2 关键概念

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" 代表 "security identity")。惟一的列是 ID,SID 的文本表示以及用于指示文本表示是引用主体名称或 GrantedAuthority 的标志。因此,每个唯一的主体或授权的权限都有一行。当在接受权限的上下文中使用时,SID 通常被称为 "recipient"。
  • 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 不直接引用域对象,而是直接指向 ObjectIdentity。Acl 存储在 ACL_OBJECT_IDENTITY 表中。
  • AccessControlEntry:Acl 拥有多个 AccessControlEntry,通常在框架中缩写为 ACE。每个 ACE 指的是许可的特定组合 Permission、Sid 和 Acl。ACE 也可以授予或不授予,并包含审计设置。ACE 被存储在 ACL_ENTRY 表中。
  • Permission:权限表示特定的不可变位掩码,并为位掩码和输出信息提供方便的功能。上面给出的基本权限(位 0 到 4)包含在 BasePermission 类中。
  • Sid:ACL 模块需要引用主体和 GrantedAuthority[]。Sid 接口提供了间接级别,它是 "security identity" 的缩写。公共类包括 PrincipalSid(代表 Authentication 对象内的主体)和 GrantedAuthoritySid。安全身份信息存储在 ACL_SID 表中。
  • ObjectIdentity:每个域对象都由 ObjectIdentity 在 ACL 模块内部进行内部表示。默认实现被称为 ObjectIdentityImpl。
  • AclService:检索适用于给定 ObjectIdentity 的 Acl。在包含的实现(JdbcAclService)中,检索操作被委托给 LookupStrategy。LookupStrategy 提供了一种高度优化的策略,用于检索 ACL 信息、使用批量检索 (BasicLookupStrategy) 和支持利用物化视图、分层查询和类似的以性能为中心的非 ANSI SQL 功能的定制实现。
  • MutableAclService:允许修改的 Acl 用于持久性。如果你不希望使用这个接口,就没有必要了。

请注意,我们的开箱即用 AclService 和相关的数据库类都使用 ANSI SQL。因此,这应该与所有主要数据库一起工作。在编写时,系统已经使用 Hypersonic SQL、PostgreSQL、Microsoft SQL Server 和 Oracle 进行了成功测试。

两个样本用 Spring Security 演示 ACL 模块。第一个是联系人示例,另一个是文档管理系统(DMS)示例。我们建议看一下这些例子。

28.3 开始使用

要开始使用 Spring Security 的 ACL 能力,你需要将 ACL 信息存储在某个地方。这就需要使用 Spring 实现 DataSource 的实例化。然后将 DataSource 注入到 JdbcMutableAclService 和 BasicLookupStrategy 实例中。后者提供高性能的 ACL 检索能力,前者提供变异器能力。参考示例 Spring Security 中的一个示例。你还需要用上一节中列出的四个特定于 ACL 的表填充数据库(有关适当的 SQL 语句,请参考 ACL 示例)。

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

下面的代码片段显示了如何创建 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" 的主体可以 "administer" 这个对象。代码片段相对自解释,除了插入方法。插入式方法的第一个参数是确定 ACL 中的什么位置将插入新的条目。在上面的例子中,我们只是把新的 ACE 放在现有 ACE 的末尾。最后一个参数是指示 ACE 是否授予或拒绝的 Boolean。大部分时间,它将授予(true),但是如果它拒绝(false),权限被有效地阻塞。

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

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

29. 预认证方案

在某些情况下,你希望使用 Spring Security 进行授权,但是用户在访问应用程序之前已经通过某个外部系统的可靠身份验证。我们将这些情况称为预认证场景。实例包括 X.509、Siteminder 和应用程序运行的 JavaEE 容器的身份验证。使用预认证时,Spring Security 必须

  • 识别请求的用户。
  • 为用户获取权限。

细节将取决于外部认证机制。对于 X.509,用户可以通过他们的证书信息进行标识,对于 Siteminder,则可以通过 HTTP 请求头进行标识。如果依赖于容器身份验证,那么将通过对传入的 HTTP 请求调用 getUserPrincipal() 方法来标识用户。在某些情况下,外部机制可以为用户提供角色/权限信息,但在其他情况下,必须从单独的源(例如 UserDetailsService)获得权限。

29.1 预认证框架类

因为大多数预身份验证机制遵循相同的模式,所以 Spring Security 具有一组类,这些类提供了用于实现预身份验证身份验证提供者的内部框架。这消除了重复,并且允许以结构化的方式添加新的实现,而不必从头开始编写所有内容。如果您想使用诸如 X.509 身份验证 之类的东西,你不需要知道这些类,因为它已经有了一个命名空间配置选项,该选项更易于使用和启动。如果需要使用显式的 bean 配置或正在计划编写自己的实现,那么了解所提供的实现如何工作将是有用的。你将在 org.springframework.security.web.authentication.preauth 下找到类。我们只是在这里提供一个大纲,所以你应该在适当的时候查阅 Javadoc 和源代码。

29.1.1 AbstractPreAuthenticatedProcessingFilter

该类将检查安全上下文的当前内容,如果为空,则尝试从 HTTP 请求中提取用户信息并将其提交给 AuthenticationManager。子类重写以下方法以获取此信息:

protected abstract Object getPreAuthenticatedPrincipal(HttpServletRequest request);

protected abstract Object getPreAuthenticatedCredentials(HttpServletRequest request);

调用这些之后,筛选器将创建包含返回数据的 PreAuthenticatedAuthenticationToken 并提交给身份验证。通过这里的身份验证,我们实际上意味着进一步处理,也许加载用户权限,但是遵循标准 Spring Security 身份验证体系结构。

与其他 Spring Security 身份验证过滤器一样,预身份验证过滤器具有 authenticationDetailsSource 属性,默认情况下,该属性将创建一个 WebAuthenticationDetails 对象以存储附加信息,如 Authentication 对象 details 属性中的会话标识符和原始 IP 地址。在可以从预身份验证机制获得用户角色信息的情况下,数据也存储在此属性中,具体实现 GrantedAuthoritiesContainer 接口。这使得认证提供者能够读取外部分配给用户的权限。接下来我们将看一个具体的例子。

J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource

如果过滤器配置有 authenticationDetailsSource,它是此类的实例,则通过为每个预定义的可映射角色集合调用 isUserInRole(String role) 方法来获得权限信息。类从配置的 MappableAttributesRetriever 获取这些。可能的实现包括在应用程序上下文中对列表进行硬编码,并从 web.xml 文件中的 <security-role> 信息读取角色信息。预认证示例应用程序使用后一种方法。

还有一个附加阶段,使用配置好的 Attributes2GrantedAuthoritiesMapper 将角色(或属性)映射到 Spring Security GrantedAuthority 对象。默认情况下,只是将常用的 ROLE_ 前缀添加到名称中,但它可以完全控制行为。

29.1.2 PreAuthenticatedAuthenticationProvider

预认证的提供者没有比用户加载 UserDetails 对象更多的事情了。它通过委派到 AuthenticationUserDetailsService 来实现这一点。后者类似于标准 UserDetailsService,但只接受 Authentication 对象而不是用户名:

public interface AuthenticationUserDetailsService {
	UserDetails loadUserDetails(Authentication token) throws UsernameNotFoundException;
}

这个接口还可以有其他用途,但是通过预身份验证,它允许访问打包在 Authentication 对象中的权限,如我们在前一节中看到的。PreAuthenticatedGrantedAuthoritiesUserDetailsService 类执行此操作。或者,它可以通过 UserDetailsByNameServiceWrapper 实现委托给一个标准的 UserDetailsService 。

29.1.3 Http403ForbiddenEntryPoint

在技术概述章中讨论了 AuthenticationEntryPoint。通常,它负责为未经身份验证的用户(当他们试图访问受保护的资源时)启动身份验证过程,但是在预先身份验证的情况下,这不适用。如果你没有结合使用预身份验证和其他身份验证机制来配置 ExceptionTranslationFilter,则只能使用此类的实例来配置。如果用户被 AbstractPreAuthenticatedProcessingFilter 拒绝,将导致 null 身份验证。如果调用,它总是返回一个 403 禁止响应代码。

29.2 具体实现

X.509 身份验证包括在它自己的章节中。这里,我们将介绍一些为其他预认证方案提供支持的类。

29.2.1 请求报头认证(Siteminder)

外部认证系统可以通过在 HTTP 请求上设置特定的报头来向应用程序提供信息。一个众所周知的例子是 Siteminder,它在名为 SM_USER 的报头中传递用户名。这种机制是由类 RequestHeaderAuthenticationFilter 支持的,它只从头文件中提取用户名。它默认使用 SM_USER 的名称作为报头名称。更多详细信息请参阅 Javadoc。

[Tip] Tip

注意,当使用这样的系统时,框架根本不执行身份验证检查,并且外部系统配置正确并保护对应用程序的所有访问是极其重要的。如果攻击者能够在其原始请求中伪造报头而不检测到这一点,那么他们可能选择他们希望的任何用户名。

Siteminder 示例配置

使用这种过滤器的典型配置看起来是这样的:

<security:http>
<!-- Additional http configuration omitted -->
<security:custom-filter position="PRE_AUTH_FILTER" ref="siteminderFilter" />
</security:http>

<bean id="siteminderFilter" class="org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter">
<property name="principalRequestHeader" value="SM_USER"/>
<property name="authenticationManager" ref="authenticationManager" />
</bean>

<bean id="preauthAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
<property name="preAuthenticatedUserDetailsService">
	<bean id="userDetailsServiceWrapper"
		class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
	<property name="userDetailsService" ref="userDetailsService"/>
	</bean>
</property>
</bean>

<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="preauthAuthProvider" />
</security:authentication-manager>

我们在这里假设安全性命名空间正在被用于配置。还假定你已经将 UserDetailsService(称为 "userDetailsService")添加到配置中以加载用户的角色。

29.2.2 Java EE 容器认证

类 J2eePreAuthenticatedProcessingFilter 将从 HttpServletRequest 的 userPrincipal 属性中提取用户名。这种过滤器的使用通常会与 JavaEE 角色的使用相结合,如上面 叫 “J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource” 的章节 所述的。

在代码库中有一个使用这种方法的示例应用程序,因此如果你感兴趣的话,可以从 github 获取代码,并查看应用程序上下文文件。代码在 samples/xml/preauth 目录中。