15. 核心安全过滤器

在使用 Spring Security 的 web 应用程序中总是使用一些关键过滤器,因此我们首先来看看这些关键过滤器及其支持类和接口。我们不会覆盖每一个特性,所以如果你想得到完整的描述,一定要查看 JavaDoc。

15.1 FilterSecurityInterceptor

在讨论一般访问控制时,我们已经简要地了解了 FilterSecurityInterceptor,并且我们已经将它与命名称空间一起使用,其中,<intercept-url> 元素组合用于内部配置它。现在我们将看到如何显式地配置它以便与 FilterChainProxy 及其配套过滤器 ExceptionTranslationFilter 一起使用。典型的配置示例如下所示:

<bean id="filterSecurityInterceptor"
	class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="securityMetadataSource">
	<security:filter-security-metadata-source>
	<security:intercept-url pattern="/secure/super/**" access="ROLE_WE_DONT_HAVE"/>
	<security:intercept-url pattern="/secure/**" access="ROLE_SUPERVISOR,ROLE_TELLER"/>
	</security:filter-security-metadata-source>
</property>
</bean>

FilterSecurityInterceptor 负责处理 HTTP 资源的安全性。它需要对 AuthenticationManager 和 AccessDecisionManager 的引用。它还提供了适用于不同 HTTP URL 请求的配置属性。请参阅技术介绍中关于这些的原始讨论。

可以通过两种方式的配置属性配置 FilterSecurityInterceptor。上面展示的第一个是使用 <filter-security-metadata-source> 命名空间元素。这与命名空间章节中的 <http> 元素类似,但 <intercept-url> 子元素仅使用 pattern 和 access 属性。逗号用于分隔应用于每个 HTTP URL 的不同配置属性。第二个选项是编写你自己的 SecurityMetadataSource,但这超出了本文档的范围。无论使用哪种方法,SecurityMetadataSource 都负责返回一个 List<ConfigAttribute>,其中包含与单个安全 HTTP URL 相关联的所有配置属性。

应当注意,FilterSecurityInterceptor.setSecurityMetadataSource() 方法实际上期望 FilterInvocationSecurityMetadataSource 的实例。这是一个子类 SecurityMetadataSource 的标记接口。它仅仅表示 SecurityMetadataSource 理解 FilterInvocation,为了简单起见,我们将继续将 FilterInvocationSecurityMetadataSource 称为 SecurityMetadataSource,因为该区别对于大多数用户来说没有什么相关性。

由命名空间语法创建的 SecurityMetadataSource 通过将请求 URL 与配置的 pattern 属性匹配来获得特定 FilterInvocation 的配置属性。这与命名空间配置的行为方式相同。默认是将所有表达式视为 Apache Ant 路径,并且对于更复杂的情况也支持正则表达式。request-matcher 属性用于指定正在使用的模式的类型。不可能将表达式语法混合到同一定义中。例如,使用正则表达式代替 Ant 路径的先前配置将被写成如下:

<bean id="filterInvocationInterceptor"
	class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="runAsManager" ref="runAsManager"/>
<property name="securityMetadataSource">
	<security:filter-security-metadata-source request-matcher="regex">
	<security:intercept-url pattern="\A/secure/super/.*\Z" access="ROLE_WE_DONT_HAVE"/>
	<security:intercept-url pattern="\A/secure/.*\" access="ROLE_SUPERVISOR,ROLE_TELLER"/>
	</security:filter-security-metadata-source>
</property>
</bean>

模式总是按照定义的顺序进行评估。因此,重要的是在列表中定义比在特定的模式中更高的特定模式。这反映在上面的示例中,其中更具体的 /secure/super/ 模式比不那么特定的 /secure/ 模式更高。如果它们被颠倒,则 /secure/ 模式将总是匹配,并且 /secure/super/ 模式将永远不会被评估。

15.2 ExceptionTranslationFilter

ExceptionTranslationFilter 位于安全过滤器堆栈中的 FilterSecurityInterceptor 之上。它本身不执行任何实际的安全强制,但是处理安全拦截器抛出的异常,并提供适当的 HTTP 响应。

<bean id="exceptionTranslationFilter"
class="org.springframework.security.web.access.ExceptionTranslationFilter">
<property name="authenticationEntryPoint" ref="authenticationEntryPoint"/>
<property name="accessDeniedHandler" ref="accessDeniedHandler"/>
</bean>

<bean id="authenticationEntryPoint"
class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<property name="loginFormUrl" value="/login.jsp"/>
</bean>

<bean id="accessDeniedHandler"
	class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
<property name="errorPage" value="/accessDenied.htm"/>
</bean>

15.2.1 AuthenticationEntryPoint

如果用户请求一个安全的 HTTP 资源,但他们未被认证,则将调用 AuthenticationEntryPoint。一个适当的 AuthenticationException 或 AccessDeniedException 将被安全拦截器抛出到调用堆栈的下方,从而触发入口点上的 commence 方法。这样做的工作是向用户呈现适当的响应,从而可以开始验证。我们这里使用的是 LoginUrlAuthenticationEntryPoint,它将请求重定向到不同的 URL(通常是登录页面)。所使用的实际实现将取决于你希望在应用程序中使用的身份验证机制。

15.2.2 AccessDeniedHandler

如果用户已经被认证并且试图访问受保护的资源,会发生什么?在正常使用中,不应该发生这种情况,因为应用程序工作流程应限于用户能够访问的操作。例如,对管理页面的 HTML 链接可能在没有管理员角色的用户中隐藏。但是,你不能依赖隐藏链接来保证安全性,因为用户总是有可能直接输入 URL 以试图绕过限制。或者他们可以修改一个 RESTful URL 来改变一些参数值。你的应用程序必须保护这些场景,否则肯定是不安全的。通常,你将使用简单的 web 层安全性来对基本 URL 应用约束,并在服务层接口上使用更具体的基于方法的安全性来真正确定允许的内容。

如果抛出 AccessDeniedException 并且用户已经被身份验证,则这意味着尝试了没有足够权限的操作。在这种情况下,ExceptionTranslationFilter 将调用第二个策略,AccessDeniedHandler。默认情况下,使用 AccessDeniedHandlerImpl,它只向客户端发送 403(禁止)响应。或者,你可以显式地配置一个实例(如上面的示例所示),并设置一个错误页面 URL,它将把请求转发到这上面 [11]。这可以是一个简单的访问拒绝页,例如 JSP,或者它可以是一个更复杂的处理程序,例如 MVC 控制器。当然,你可以自己实现接口并使用自己的实现。

当你使用命名空间来配置应用程序时,也可以提供自定义 AccessDeniedHandler。有关的详细信息请参阅命名空间附录。

15.2.3 SavedRequest 和 the RequestCache 接口

ExceptionTranslationFilter 职责中的另一个职责是在调用 AuthenticationEntryPoint 之前保存当前请求。这允许在用户已验证后恢复该请求(参阅 Web 身份验证的先前概述)。典型的例子是用户以表单登录,然后通过默认 SavedRequestAwareAuthenticationSuccessHandler 重定向到原始URL(参阅下文)。

RequestCache 封装了存储和检索 HttpServletRequest 实例所需的功能。默认情况下,使用 HttpSessionRequestCache,它将请求存储在 HttpSession 中。RequestCacheFilter 的任务是当用户被重定向到原始 URL 时,从缓存中实际恢复保存的请求。

在正常情况下,你不需要修改任何此功能,但是保存请求处理是一种尽力而为的方法,并且可能存在默认配置无法处理的情况。这些接口的使用使得它完全可以从 Spring Security 3.0 向前兼容。

15.3 SecurityContextPersistenceFilter

我们在技术概览章节中讨论了这个非常重要的过滤器的用途,所以你现在可能需要重新阅读这一节。让我们先看看如何配置它以与 FilterChainProxy 一起使用。基本配置只需要 bean 本身。

<bean id="securityContextPersistenceFilter"
class="org.springframework.security.web.context.SecurityContextPersistenceFilter"/>

正如我们前面看到的,这个过滤器有两个主要任务。它负责在 HTTP 请求之间存储 SecurityContext 内容,并在完成请求时清除 SecurityContextHolder。清除存储上下文的 ThreadLocal 是必要的,否则可能将线程替换到 servlet 容器的线程池中,同时仍然附加特定用户的安全上下文。然后,这个线程可以在以后的阶段使用,用错误的凭证执行操作。

15.3.1 SecurityContextRepository

从 Spring Security 3.0 起,加载和存储安全上下文的任务现在委托给单独的策略接口:

public interface SecurityContextRepository {

SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder);

void saveContext(SecurityContext context, HttpServletRequest request,
		HttpServletResponse response);
}

HttpRequestResponseHolder 只是传入请求和响应对象的容器,允许实现用包装器类替换它们。返回的内容将传递到过滤器链。

默认实现是 HttpSessionSecurityContextRepository,它将安全上下文存储为 HttpSession 属性 [12]。此实现的最重要的配置参数是 allowSessionCreation 属性,默认为 true,因此如果类需要为经过身份验证的用户存储安全上下文,则允许该类创建会话(除非进行了身份验证,否则不会创建会话)。安全上下文的位置和内容已经改变。如果不希望创建会话,则可以将此属性设置为 false:

<bean id="securityContextPersistenceFilter"
	class="org.springframework.security.web.context.SecurityContextPersistenceFilter">
<property name='securityContextRepository'>
	<bean class='org.springframework.security.web.context.HttpSessionSecurityContextRepository'>
	<property name='allowSessionCreation' value='false' />
	</bean>
</property>
</bean>

或者,可以提供 NullSecurityContextRepository 的实例,它是一个空对象实现,即使请求期间已经创建了会话,它也可以防止存储安全上下文。

15.4 UsernamePasswordAuthenticationFilter

现在我们已经看到了三个主要的过滤器,它们总是存在于 Spring Security web 配置中。这三者也是由命名空间 <http> 元素自动创建的,并且不能用替代品替换。现在唯一缺少的是一个实际的身份验证机制,它允许用户进行身份验证。这个过滤器是最常用的验证过滤器,也是最常被定制的过滤器 [13]。它还提供了从命名空间中使用的 <form-login> 元素的实现。配置它需要三个阶段。

  • 与上面一样,使用登录页面的 URL 配置 LoginUrlAuthenticationEntryPoint,并将其设置为 ExceptionTranslationFilter。
  • 实现登录页面(使用 JSP 或 MVC 控制器)。
  • 在应用程序上下文中配置 UsernamePasswordAuthenticationFilter 实例
  • 将过滤器 bean 添加到过滤器链代理(确保你注意顺序)。

登录表单仅包含 username 和 password 输入字段,并张贴到过滤器监听的 URL(默认情况下为 /login)。基本的过滤器配置看起来像这样:

<bean id="authenticationFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager"/>
</bean>

15.4.1 身份验证成功与失败的应用流程

过滤器调用配置的 AuthenticationManager 来处理每个身份验证请求。身份验证成功或身份验证失败之后的目的地分别由 AuthenticationSuccessHandler 和 AuthenticationFailureHandler 策略接口控制。过滤器具有允许设置这些属性的属性,因此可以完全自定义行为 [14]。提供了一些标准实现,例如 SimpleUrlAuthenticationSuccessHandler、SavedRequestAwareAuthenticationSuccessHandler、SimpleUrlAuthenticationFailureHandler、ExceptionMappingAuthenticationFailureHandler 和 DelegatingAuthenticationFailureHandler。看一下这些类的 Javadoc,以及 AbstractAuthenticationProcessingFilter,了解它们的工作方式和支持的特性。

如果身份验证成功,则生成的 Authentication 对象将被放置到 SecurityContextHolder 中。然后,将调用配置好的 AuthenticationSuccessHandler 来将用户重定向或转发到适当的目的地。默认情况下,使用 SavedRequestAwareAuthenticationSuccessHandler,这意味着用户在被要求登录之前将被重定向到他们请求的原始目的地。

[Note] Note

ExceptionTranslationFilter 缓存用户发出的原始请求。当用户进行身份验证时,请求处理程序利用该缓存请求获取原始 URL 并重定向到该请求。然后重建原始请求并作为备选方案使用。

如果身份验证失败,将调用配置的 AuthenticationFailureHandler。



[11] 我们使用转发,以便 SecurityContextHolder 仍然包含主体的细节,这对于向用户显示可能是有用的。在老版本的 Spring Security 中,我们依赖于 servlet 容器来处理 403 错误消息,而后者缺少这个有用的上下文信息。

[12] 在 Spring Security 2.0 及更早版本中,这个过滤器被称为 HttpSessionContextIntegrationFilter,并且执行了存储上下文的所有工作,这些工作是由过滤器本身执行的。如果你熟悉这个类,那么现在可以在 HttpSessionSecurityContextRepository 上找到可用的大多数配置选项。

[13] 由于历史原因,在 Spring Security 3.0 之前,这个过滤器被称为 AuthenticationProcessingFilter,入口点称为 AuthenticationProcessingFilterEntryPoint。由于框架现在支持许多不同形式的认证,所以它们在 3.0 中都被赋予了更具体的名称。

[14] 在 3.0 之前的版本中,此时的应用程序流程已经发展到一个阶段,由这个类和策略插件上的属性混合控制。3.0 的决定是对代码进行重构,使这两种策略完全负起责任。