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

过滤器链

DelegatingFilterProxy

使用 Servlet 过滤器时,显然需要在 web.xml 中声明它们,否则 Servlet 容器将忽略它们。

在 Spring Security 中,过滤器类也是在应用程序上下文中定义的 Spring bean,因此能够利用 Spring 丰富的依赖注入工具和生命周期接口。Spring 的 DelegatingFilterProxy 提供了 web.xml 和应用程序上下文之间的链接。它是通过 WebApplicationInitializer 初始化器在构建应用上下文的过程中以编程的方式将该 Servlet 过滤器注册到 ServletContext 中,相当于:

  <filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>
  
  <filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

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

@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 允许我们向 Spring Security 内部过滤器链添加一个过滤器 Bean,并完全处理应用程序上下文文件以管理我们的 Web 安全。

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

@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);

    // 获取对应该请求的过滤器链,默认只有一个过滤器链:DefaultSecurityFilterChain
    // 我们也可以我不同的请求使用不同的过滤器链
    List<Filter> filters = getFilters(fwRequest);

    if (filters == null || filters.size() == 0) {
        ...
        fwRequest.reset();

        // 继续 servlet 的过滤器链调用
        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 属性,可以使用以下命令:

@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:它使用 HttpServletRequestWrapper 包裹 servlet 的 ServletRequest 请求,提供安全认证请求的额外功能。
  • JaasApiIntegrationFilter:JAAS 全称为 Java Authentication Authorization Service,中文含义即 Java 认证和授权服务。使用可插入方式将认证和授权逻辑和应用程序分离开。如果 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 可以通过两种方式配置配置属性。第一种,是使用命名空间元素 或者通过配置 HttpSecurity 构建器,这里不再说明。第二个选项是编写自己的 SecurityMetadataSource,无论使用何种方法。SecurityMetadataSource 负责返回 List,其中包含与单个安全 HTTP URL 关联的所有配置属性。

应该注意的是,FilterSecurityInterceptor.setSecurityMetadataSource() 方法实际上需要 FilterInvocationSecurityMetadataSource 的实例,它是一个标记接口,表示它是 SecurityMetadataSource 的子类。它只是表示 SecurityMetadataSource 了解 FilterInvocation。为了简单起见,我们将继续将 FilterInvocationSecurityMetadataSource 称为 SecurityMetadataSource,因为这种区别与大多数用户没什么关系。

由命名空间语法或构建器创建的 SecurityMetadataSource 通过将请求 URL 与配置的 pattern 属性相匹配来获取特定 FilterInvocation 的配置属性。缺省情况是将所有表达式视为 Apache Ant 路径,并且对于更复杂的情况也支持正则表达式。request-matcher 属性用于指定正在使用的模式的类型。在同一定义中无法混合表达式语法。

FilterInvocationSecurityMetadataSource 内部有两种默认实现:

  • DefaultFilterInvocationSecurityMetadataSource

    FilterInvocationSecurityMetadataSource 的默认实现。 将 RequestMatcher 的有序映射存储到 ConfigAttribute 集合,并提供 FilterInvocation 与存储在映射中的项目的匹配。

    我们可以通过 UrlAuthorizationConfigurer 配置类添加基于 URL 的授权元数据。

    用法包括应用 UrlAuthorizationConfigurer ,然后修改 StandardInterceptUrlRegistry。例如通过配置构建器:

    protected void configure(HttpSecurity http) throws Exception {
      	http.apply(new UrlAuthorizationConfigurer<HttpSecurity>()).getRegistry()
      			.antMatchers("/users**", "/sessions/**").hasRole("USER")
      			.antMatchers("/signup").hasRole("ANONYMOUS")
            .anyRequest().hasRole("USER");
    }
    
  • ExpressionBasedFilterInvocationSecurityMetadataSource

    该实现继承自 DefaultFilterInvocationSecurityMetadataSource 默认实现,并扩展支持基于表达式的授权元数据。

    同样我们也可以使用 ExpressionUrlAuthorizationConfigurer 配置类将基于 SpEL 表达式的基于 URL 的授权添加到应用程序。

    该配置类继承了默认 Url 配置类的能力,并内置了一些常见授权用法的 Spel 方法,例如:

    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
              .antMatchers("/login*", "/logout*").permitAll()
              .antMatchers("/protectedbyrole").hasRole("USER")
              .antMatchers("/protectedbyauthority").hasAuthority("READ_PRIVILEGE")
              .antMatchers("/protectedByIp").not().hasIpAddress("192.168.0/24");
    

    我们也可以直接使用 SpEL 表达式语法实现更复杂的访问控制,例如组合多个表达式:

    http
        .authorizeHttpRequests()
    			.antMatchers("/foos/**").access("isAuthenticated() and hasIpAddress('11.11.11.11')")
    

    或者交给自定义的业务类 bean 来自行控制逻辑,比如基于数据库授权元数据的评估:

    假设我们有一个名为 webSecurity 的 Bean,其中包含以下方法签名:

    public class WebSecurity {
    		public boolean check(Authentication authentication, HttpServletRequest request) {
    				...
    		}
    }
    

    使用 SpEL 表达式扩展可用的授权表达式:

    http
        .authorizeHttpRequests()
          // 使用 websecurity bean 的 check 方法自行判断
          .antMatchers("/user/**").access("@webSecurity.check(authentication, request)")  
          ...
    

调用将始终按照定义的顺序评估模式。在列表中定义的更具体的模式比不太具体的模式更高这一点很重要。

ExceptionTranslationFilter

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

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。

Ant-style 映射使用以下规则匹配 URL:

通配符描述
?只匹配一个字符
*匹配零个或多个字符
**匹配路径中的零个或多个“目录”
{spring:[a-z]+}使用正则 [a-z]+ 匹配名为“spring”的路径变量

Examples:

ExampleMatches:
com/t?st.jspcom/test.jsp、com/tast.jsp、com/txst.jsp
com/*.jspcom 目录下的所有 .jsp 文件
com/**/test.jspcom 路径下的所有 test.jsp 文件
org/springframework/**/*.jsporg/springframework 路径下的所有 .jsp 文件
org/**/servlet/bla.jsporg/springframework/servlet/bla.jsp
also:org/springframework/testing/servlet/bla.jsp
also:org/servlet/bla.jsp
com/{filename:\w+}.jspcom/test.jsp & 将值 test 分配给 filename 变量

如果你需要一个更加强大的模式匹配,你可以使用正则表达式。这种策略的实现是 RegexRequestMatcher

HttpFirewall 还通过拒绝 HTTP 响应标头中的换行字符来阻止 HTTP 响应拆分。 默认情况下使用 StrictHttpFirewall。此实现拒绝看似恶意的请求。你也可以自定义那些类型的请求应该被拒绝。比如,如果你希望利用 Spring MVC 的矩阵变量,你可以这样配置:

@Bean
public StrictHttpFirewall httpFirewall() {
    StrictHttpFirewall firewall = new StrictHttpFirewall();
    firewall.setAllowSemicolon(true);
    return firewall;
}

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

@Bean
public StrictHttpFirewall httpFirewall() {
    StrictHttpFirewall firewall = new StrictHttpFirewall();
    firewall.setAllowedHttpMethods(Arrays.asList("GET", "POST"));
    return firewall;
}

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