Spring Security 过滤器

Spring Security 的 Web 基础结构完全基于标准的 servlet 过滤器。Spring Security 在内部维护一个过滤器链,其中每个过滤器都有特定的责任,过滤器的顺序很重要,因为它们之间存在依赖关系。

过滤器链

  • DelegatingFilterProxy

    使用 servlet 过滤器时,显然需要在 web.xml 中声明它们,否则 servlet 容器将忽略它们。在 Spring Security 中,过滤器类也是在应用程序上下文中定义的 Spring bean,因此能够利用 Spring 丰富的依赖注入工具和生命周期接口。Spring 的 DelegatingFilterProxy 提供了 web.xml 和应用程序上下文之间的链接。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <filter>
    <filter-name>myFilter</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>

    <filter-mapping>
    <filter-name>myFilter</filter-name>
    <url-pattern>/*</url-pattern>
    </filter-mapping>

    请注意,过滤器实际上是 DelegatingFilterProxy,而不是实际实现过滤器逻辑的类。DelegatingFilterProxy 所做的是将 Filter 的方法委托给从 Spring 应用程序上下文中获取的 bean。bean 必须实现 javax.servlet.Filter,它必须与 filter-name 元素中的名称相同。
    DelegatingFilterProxy 继承 GenericFilterBean,该抽象类实现 Filter 接口,并提供 Spring 的管理。但是委托类自己不去实现安全过滤,而是将过滤方法委托给 FilterChainProxy 代理类去做。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

// Lazily initialize the delegate if necessary.
Filter delegateToUse = this.delegate;
if (delegateToUse == null) {
synchronized (this.delegateMonitor) {
delegateToUse = this.delegate;
if (delegateToUse == null) {
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
throw new IllegalStateException("No WebApplicationContext found: " +
"no ContextLoaderListener or DispatcherServlet registered?");
}
delegateToUse = initDelegate(wac);
}
this.delegate = delegateToUse;
}
}

// Let the delegate perform the actual doFilter operation.
invokeDelegate(delegateToUse, request, response, filterChain);
}

这里的 delegateToUse 就是 FilterChainProxy,代理类调用自己的 Filter 实现。

  • FilterChainProxy

    Spring Security 的 Web 基础结构只能通过委托 FilterChainProxy 实例来实现。安全过滤器不应该使用自身。FilterChainProxy 允许我们向 web.xml 添加一个条目,并完全处理应用程序上下文文件以管理我们的 Web 安全 bean。

    该类同样继承自 GenericFilterBeanFilter 实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
if (clearContext) {
try {
request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
doFilterInternal(request, response, chain);
}
finally {
SecurityContextHolder.clearContext();
request.removeAttribute(FILTER_APPLIED);
}
}
else {
doFilterInternal(request, response, chain);
}
}

private void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {

FirewalledRequest fwRequest = firewall
.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse fwResponse = firewall
.getFirewalledResponse((HttpServletResponse) response);

List<Filter> filters = getFilters(fwRequest);

if (filters == null || filters.size() == 0) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(fwRequest)
+ (filters == null ? " has no matching filters"
: " has an empty filter list"));
}

fwRequest.reset();

chain.doFilter(fwRequest, fwResponse);

return;
}

VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
}

这里的 firewall 就实现了安全字符过滤,Url编码解码配置,访问方法配置等等安全策略。
真正执行安全过滤的是在其内部类 VirtualFilterChain 中,在该类中依次调用各个安全过滤器。

后处理配置实体 - ObjectPostProcessor

Spring Security 的 Java 配置不会公开它配置的每个对象的每个属性。这简化了大多数用户的配置。毕竟,如果每个属性都被暴露,用户可以使用标准 bean 配置。

虽然有充分的理由不直接公开每个属性,但用户可能仍需要更高级的配置选项。为了解决这个问题,Spring Security 引入了 ObjectPostProcessor 的概念,可用于修改或替换 Java Configuration 创建的许多 Object 实例。例如,如果要在 FilterSecurityInterceptor 上配置 filterSecurityPublishAuthorizationSuccess 属性,可以使用以下命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
public <O extends FilterSecurityInterceptor> O postProcess(O fsi) {
// 默认只会广播 AuthorizationFailureEvent 事件,如果设置为 true,则同时也会广播 AuthorizedEvent
fsi.setPublishAuthorizationSuccess(true);
return fsi;
}
});
}

过滤器顺序

过滤器在链中定义的顺序非常重要:

  • ChannelProcessingFilter:因为它可能需要重定向到不同的协议
  • SecurityContextPersistenceFilter: 因此,可以在 Web 请求开始时在 SecurityContextHolder 中设置 SecurityContext,并且当 Web 请求结束时(可以使用下一个 Web 请求准备好),可以将对 SecurityContext 的任何更改复制到 HttpSession。
  • ConcurrentSessionFilter: 因为它需要使用 SecurityContextHolder 的功能,而且更新对应 session 的最后更新时间,以及通过 SessionRegistry 获取当前的 SessionInformation 以检查当前的 session 是否已经过期,过期则会调用 LogoutHandler。
  • 身份验证处理机制 - UsernamePasswordAuthenticationFilterCasAuthenticationFilterBasicAuthenticationFilter 等 - 以便可以修改 SecurityContextHolder 以包含有效的身份验证请求令牌。
  • SecurityContextHolderAwareRequestFilter:使用它将 Spring Security 感知 HttpServletRequestWrapper 安装到您的 servlet 容器中。
  • JaasApiIntegrationFilter:如果 JaasAuthenticationToken 位于SecurityContextHolder 中,则会将 FilterChain 作为 JaasAuthenticationToken 中的 Subject 进行处理。
  • RememberMeAuthenticationFilter: 如果没有更早的身份验证处理机制更新 SecurityContextHolder,并且该请求提供了一个 cookie,使我能够记住我的服务,一个合适的 remembered Authentication 验证对象将会设给 SecurityContextHolder。
  • AnonymousAuthenticationFilter,这样如果没有早期的身份验证处理机制更新 SecurityContextHolder,那么该安全上下文将被匿名身份验证对象填充。
  • ExceptionTranslationFilter,用于捕获任何 Spring Security 异常,以便可以返回 HTTP 错误响应或启动相应的 AuthenticationEntryPoint。
  • FilterSecurityInterceptor,用于保护 Web URI 并在访问被拒绝时引发异常。

核心过滤器

  • FilterSecurityInterceptor

该过滤器负责处理 HTTP 资源的安全性,它需要一个 AuthenticationManagerAccessDecisionManager 的引用。它还提供了适用于不同 HTTP URL 请求的配置属性。
FilterSecurityInterceptor 可以通过两种方式配置配置属性。第一种,是使用命名空间元素 ,这里不再说明。第二个选项是编写自己的 SecurityMetadataSource,无论使用何种方法。SecurityMetadataSource 负责返回 List,其中包含与单个安全 HTTP URL 关联的所有配置属性。
应该注意的是,FilterSecurityInterceptor.setSecurityMetadataSource() 方法实际上需要 FilterInvocationSecurityMetadataSource 的实例。它是一个标记接口,表示它是 SecurityMetadataSource 的子类。它只是表示 SecurityMetadataSource 了解 FilterInvocation。为了简单起见,我们将继续将 FilterInvocationSecurityMetadataSource 称为 SecurityMetadataSource,因为这种区别与大多数用户没什么关系。
由命名空间语法创建的 SecurityMetadataSource 通过将请求 URL 与配置的 pattern 属性相匹配来获取特定 FilterInvocation 的配置属性。这与命名空间配置的行为方式相同。缺省情况是将所有表达式视为 Apache Ant 路径,并且对于更复杂的情况也支持正则表达式。request-matcher 属性用于指定正在使用的模式的类型。在同一定义中无法混合表达式语法。
始终按照定义的顺序评估模式。因此,在列表中定义的更具体的模式比不太具体的模式更高这一点很重要。

  • ExceptionTranslationFilter

    ExceptionTranslationFilter 位于安全过滤器堆栈中的 FilterSecurityInterceptor 之上。它不执行任何实际的安全实施,但处理安全拦截器抛出的异常并提供合适的 HTTP 响应。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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>
  • AuthenticationEntryPoint

用户未进行身份验证时请求安全的 HTTP 资源时,会调用 AuthenticationEntryPoint。安全拦截器将在调用堆栈的下方抛出适当的 AuthenticationExceptionAccessDeniedException,触发入口点的 commence 方法。这样做的目的是向用户提供适当的响应,以便开始身份验证。我们在这里使用的是 LoginUrlAuthenticationEntryPoint,它将请求重定向到不同的URL(通常是登录页面)。使用的实际实现将取决于您希望在应用程序中使用的身份验证机制。

  • AccessDeniedHandler

    如果抛出 AccessDeniedException 并且用户已经过身份验证,则这意味着此操作没有足够权限。在这种情况下,ExceptionTranslationFilter 将调用第二个策略 AccessDeniedHandler。默认情况下,使用 AccessDeniedHandlerImpl,它只向客户端发送 403(Forbidden)响应。你也可以实现自己的处理。
  • SavedRequest RequestCache 接口

ExceptionTranslationFilter 职责的另一个职责是在调用 AuthenticationEntryPoint 之前保存当前请求。这允许在用户进行身份验证后恢复请求,一个典型的例子是用户使用表单登录,然后通过默认的 SavedRequestAwareAuthenticationSuccessHandler 重定向到原始 URL。
RequestCache 封装了存储和检索 HttpServletRequest 实例所需的功能。默认使用 HttpSessionRequestCache,它将请求存储在 HttpSession 中。当用户被重定向到原始 URL 时,RequestCacheFilter 的作用是实际从缓存中恢复已保存的请求。

  • SecurityContextPersistenceFilter

    根据应用程序的类型,可能需要采用策略来在用户操作之间存储安全上下文。在典型的Web应用程序中,用户登录一次,然后由其 session Id 标识。服务器在会话期间缓存主体信息。在 Spring Security 中,在请求之间存储 SecurityContext 的责任属于SecurityContextPersistenceFilter,它默认将上下文存储为HTTP请求之间的 HttpSession 属性。它为每个请求恢复 SecurityContextHolder 的上下文,并且至关重要的是,在请求完成时清除 SecurityContextHolder。出于安全目的,您不应直接与 HttpSession 交互,使用 SecurityContextHolder 即可。
    许多其他类型的应用程序(例如,无状态 RESTful Web 服务)不使用 HTTP 会话,并将在每个请求上重新进行身份验证。但是,在链中包含 SecurityContextPersistenceFilter 以确保在每次请求后清除 SecurityContextHolder 仍然很重要。
    如前所述,此过滤器有两个主要任务。它负责在 HTTP 请求之间存储 SecurityContext 内容,并在请求完成时清除 SecurityContextHolder。清除存储上下文的 ThreadLocal 是必不可少的,因为否则可能会将一个线程替换为 servlet 容器的线程池,与特定用户的安全上下文仍然附加。然后可以在稍后阶段使用该线程,使用错误的凭证执行操作。
    从 Spring Security 3.0 开始,加载和存储安全上下文的工作现在被委托给一个单独的策略接口 SecurityContextRepository

  • UsernamePasswordAuthenticationFilter

    我们现在已经看到了 Spring Security Web 配置中始终存在的三个主要过滤器。现在唯一缺少的是实际的身份验证机制,允许用户进行身份验证。此过滤器是最常用的身份验证过滤器,也是最常定制的过滤器。配置它需要三个阶段。

    • 使用登录页面的URL来配 LoginUrlAuthenticationEntryPoint,就像我们上面所做的那样,并在 ExceptionTranslationFilter 上设置它。
    • 实现登录页面(使用 JSP 或 MVC 控制器)。
    • 在应用程序上下文中配置 UsernamePasswordAuthenticationFilter 的实例。
    • 将过滤器 bean 添加到过滤器链代理(确保您注意顺序)。

认证成功与失败的应用流程

过滤器调用配置 AuthenticationManager 来处理每个身份验证请求。身份验证成功或身份验证失败后的目标分别由 AuthenticationSuccessHandlerAuthenticationFailureHandler 策略接口控制。分别的,过滤器具有这些属性以便您可以完全自定义行为。提供了一些标准实现,如 SimpleUrlAuthenticationSuccessHandler, SavedRequestAwareAuthenticationSuccessHandler, SimpleUrlAuthenticationFailureHandler, ExceptionMappingAuthenticationFailureHandler and DelegatingAuthenticationFailureHandler。查看这些类的 Javadoc 以及 AbstractAuthenticationProcessingFilter,以了解它们的工作原理和支持的功能。

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

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

请求匹配和 HttpFirewall

Servlet 规约为 HttpServletRequest 定义了一些属性,我们可能希望与之匹配来验证安全。这些是 contextPathservletPathpathinfoqueryString。Spring Security 只对保护应用程序中的路径感兴趣,因此将忽略 contextPath。但是 serveltPath 和 pathInfo 没有明确规范路径如何定义,比如每段地址是否都可以包含参数。为了防止这些问题, FilterChainProxy 使用 HttpFirewall 策略去检查和包裹请求。默认情况下,未规范化的请求会自动被拒绝,删除路径参数和重复斜杠以进行匹配。因此,必须使用 FilterChainProxy 来管理安全过滤器链。
如上所述,默认策略是使用 Ant-style 路径进行匹配,这可能是大多数用户的最佳选择。该策略在 AntPathRequestMatcher 类中实现,该类使用 Spring 的 AntPathMatcher 对 servletPath 和 pathInfo 执行不区分大小写的模式匹配,忽略 queryString。
如果你需要一个更加强大的模式匹配,你可以使用正则表达式。这种策略的实现是 RegexRequestMatcher
HttpFirewall 还通过拒绝 HTTP 响应标头中的换行字符来阻止 HTTP 响应拆分
默认情况下使用 StrictHttpFirewall。此实现拒绝看似恶意的请求。你也可以自定义那些类型的请求应该被拒绝。比如,如果你希望利用 Spring MVC 的矩阵变量,你可以这样配置:

1
2
3
4
5
6
@Bean
public StrictHttpFirewall httpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowSemicolon(true);
return firewall;
}

StrictHttpFirewall 提供有效 HTTP 方法的白名单,允许防止跨站点跟踪(XST)和 HTTP 动词篡改。默认有效的方法是"DELETE",“GET”, “HEAD”,“OPTIONS”,“PATCH”,“POST”,and “PUT”。如果你希望修改有效的有效方法,可以这样配置:

1
2
3
4
5
6
@Bean
public StrictHttpFirewall httpFirewall() {
StrictHttpFirewall firewall = new StrictHttpFirewall();
firewall.setAllowedHttpMethods(Arrays.asList("GET", "POST"));
return firewall;
}

如果必须允许任何 HTTP 方法(不推荐),则可以使用 StrictHttpFirewall.setUnsafeAllowAnyHttpMethod(true)。这将完全禁用 HTTP 方法的验证。