章节 IV. Web 应用程序安全

大多数 Spring Security 用户将在使用 HTTP 和 Servlet API 的应用程序中使用框架。在本部分中,我们将了解 Spring Security 如何为应用程序的 web 层提供身份验证和访问控制特性。我们将在命名空间的外观后面进行查看,并查看实际组装了哪些类和接口来提供 web 层安全性。在某些情况下,有必要使用传统的 bean 配置来提供对配置的完全控制,因此我们还将了解如何在没有命名空间的情况下直接配置这些类。

14. 安全过滤器链

Spring Security 的 web 基础设施完全基于标准的 servlet 过滤器。它在内部不使用 servlet 或任何其它基于 servlet 的框架(如 Spring MVC),因此它没有与任何特定 web 技术的强链接。它处理 HttpServletRequest 和 HttpServletResponse,并不关心这些请求是来自浏览器、web 服务客户端、HttpInvoker 还是 AJAX 应用程序。

Spring Security 在内部维护一个过滤器链,其中每个过滤器具有特定的责任,并且根据需要哪些服务从配置中添加或删除过滤器。过滤器的排序很重要,因为它们之间存在依赖关系。如果你一直使用命名空间配置,那么过滤器将为你自动配置,并且你不必显式地定义任何 Spring bean,但是有时候你需要完全控制安全过滤器链,因为你使用的特性不是在命名空间中进行支持,或者使用自己自定义的类版本。

14.1 DelegatingFilterProxy

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

当使用 DelegatingFilterProxy e时,你将在 web.xml 文件中看到类似的内容:

<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 受益于 Spring web 应用程序上下文生命周期支持和配置灵活性。bean 必须实现 javax.servlet.Filter ,它必须与 filter-name 元素中的名称相同。请阅读 DelegatingFilterProxy 的 JavaDoc 以获得更多详细信息

14.2 FilterChainProxy

Spring Security 的 web 基础结构只应通过委派给 FilterChainProxy 的实例来使用。安全过滤器不应该被自己使用。理论上,你可以在应用程序上下文文件中声明你需要的每个 Spring Security 过滤器 bean,并为每个过滤器向 web.xml 添加相应的 DelegatingFilterProxy 条目,确保它们被正确排序,但是如果你有很多过滤器,这样做会很麻烦,并且会弄乱 web.xml文件。FilterChainProxy 允许我们向 web.xml 添加一个条目,并完全处理用于管理 web 安全 bean 的应用程序上下文文件。它使用 DelegatingFilterProxy 进行链接,与上面的示例中一样,但是过滤器名称设置为 bean 名称 "filterChainProxy"。然后在具有相同 bean 名称的应用程序上下文中声明过滤器链。下面是一个例子:

<bean id="filterChainProxy" class="org.springframework.security.web.FilterChainProxy">
<constructor-arg>
	<list>
	<sec:filter-chain pattern="/restful/**" filters="
		securityContextPersistenceFilterWithASCFalse,
		basicAuthenticationFilter,
		exceptionTranslationFilter,
		filterSecurityInterceptor" />
	<sec:filter-chain pattern="/**" filters="
		securityContextPersistenceFilterWithASCTrue,
		formLoginFilter,
		exceptionTranslationFilter,
		filterSecurityInterceptor" />
	</list>
</constructor-arg>
</bean>

命名空间元素 filter-chain 用于方便地建立应用程序中所需的安全过滤器链。[6]。它将特定的 URL 模式映射到由 filters 元素中指定的 bean 名称构建的过滤器列表,并将它们组合到 SecurityFilterChain 类型的 bean 中。模式属性采用一个 Ant 路径,最具体的 URI 应该出现在第一个 [7]。在运行时,FilterChainProxy 将定位与当前 web 请求匹配的第一个 URI 模式,过滤器属性指定的过滤器 bean 列表将应用于该请求。将按照定义过滤器的顺序调用过滤器,因此你可以完全控制应用于特定 URL 的过滤器链。

你可能已经注意到我们已经在过滤器链中声明了两个 SecurityContextPersistenceFilter(ASC 是 allowSessionCreation 的缩写,SecurityContextPersistenceFilter 的属性)。由于 web 服务永远不会在将来的请求上呈现 jsessionid ,因此为这些用户代理创建 HttpSession 是浪费的。如果你有一个需要最大可伸缩性的大容量应用程序,我们建议你使用上面所示的方法。对于较小的应用程序,使用单个 SecurityContextPersistenceFilter(其默认 allowSessionCreation 为 true)可能就足够了。

请注意,FilterChainProxy 不在配置的过滤器上调用标准的过滤器生命周期方法。我们建议你使用 Spring 的应用程序上下文生命周期接口作为替代,就像任何其它 Spring bean 一样。

当我们研究如何使用命名空间配置设置 web 安全时,我们使用了名为 "springSecurityFilterChain" 的 DelegatingFilterProxy。现在你应该可以看到,这是由命名空间创建的 FilterChainProxy 的名称。

14.2.1 绕过过滤器链

可以使用属性 filters = "none" 作为提供过滤 bean 列表的替代方案。这将完全省略安全过滤器链中的请求模式。注意,与此路径匹配的任何内容都不会应用任何身份验证或授权服务,并且可以自由访问。如果希望在请求期间使用 SecurityContext 的内容,那么它必须已经通过安全过滤器链。否则,SecurityContextHolder 将不会被填充,内容将为 null。

14.3 过滤器排序

在链中定义过滤器的顺序是非常重要的。不管你实际使用哪种过滤器,顺序应该如下:

  • ChannelProcessingFilter,因为它可能需要重定向到不同的协议
  • SecurityContextPersistenceFilter,因此,可以在 web 请求开始时在 SecurityContextHolder 中设置 SecurityContext,并且当 web 请求结束时,可以将 SecurityContext 的任何更改复制到 HttpSession(准备与下一个 web 请求一起使用)
  • ConcurrentSessionFilter,因为它使用 SecurityContextHolder 功能,并且需要更新 SessionRegistry 以反映来自主体的持续请求
  • 身份认证处理机制 - UsernamePasswordAuthenticationFilter、CasAuthenticationFilter、BasicAuthenticationFilter 等 - 因此,可以修改 SecurityContextHolder 以包含有效的 Authentication 请求令牌。
  • SecurityContextHolderAwareRequestFilter,如果你正在使用它来将 Spring Security 感知的 HttpServletRequestWrapper 安装到 servlet 容器中
  • JaasApiIntegrationFilter,如果 JaasAuthenticationToken 位于 SecurityContextHolder 中,则会将 FilterChain 作为 JaasAuthenticationToken 中的 Subject 进行处理
  • RememberMeAuthenticationFilter,因此,如果没有更早的身份认证处理机制更新 SecurityContextHolder,并且该请求呈现一个 cookie,该 cookie 允许执行记住我服务,那么将放置一个合适的记住 Authentication 对象
  • AnonymousAuthenticationFilter,因此,如果没有早期的身份验证处理机制更新 SecurityContextHolder,则将放置匿名 Authentication 对象
  • ExceptionTranslationFilter,来捕获任何 Spring Security 异常,以便返回 HTTP 错误响应或者启动适当的 AuthenticationEntryPoint
  • FilterSecurityInterceptor,在拒绝访问时保护 web URI 并引发异常

14.4 请求匹配与 HttpFirewall

Spring Security 有几个区域,你定义的模式针对传入的请求进行测试,以便决定如何处理请求。当 FilterChainProxy 决定一个请求应该通过哪个过滤器链时,以及当 FilterSecurityInterceptor 决定对请求应用哪些安全约束时,就会发生这种情况。重要的是了解机制是什么,以及在测试你定义的模式时使用什么 URL 值。

Servlet 规范为 HttpServletRequest 定义了几个属性,这些属性可以通过 getter 方法访问,并且我们希望与之匹配。它们是 contextPath、servletPath、pathInfo 和 queryString。Spring Security 在应用程序保护路径是唯一的兴趣,所以 contextPath 是忽略的。不幸的是,servlet 规范没有确切地定义 servletPath 和 pathInfo 的值对于特定请求 URI 将包含什么。例如,URL 的每个路径段可以包含如 RFC 2396 中定义的参数 [8]。规范没有明确说明这些是否应该包含在 servletPath 和 pathInfo 值中,并且在不同的 servlet 容器之间行为有所不同。当应用程序部署在不从这些值中去除路径参数的容器中时,攻击者可能会将它们添加到所请求的 URL,以便导致模式匹配意外地成功或失败。[9]。输入 URL 中的其它变化也是可能的。例如,它可能包含路径遍历序列(如 /../)或多个正斜杠(//),这也可能导致模式匹配失败。一些容器在执行 servlet 映射之前将这些规范化,但其它容器则不这样做。为了避免类似的问题,FilterChainProxy 使用 HttpFirewall 策略来检查和包装请求。默认情况下,非规范化请求被自动拒绝,并且出于匹配的目的删除路径参数和重复斜杠。[10]。因此,必须使用 FilterChainProxy 来管理安全过滤器链。注意,servletPath 和 pathInfo 值由容器解码,因此应用程序不应该具有任何包含分号的有效路径,因为这些部分将被移除,以便进行匹配。

如上所述,默认策略是使用 Ant 样式的路径进行匹配,这对于大多数用户来说可能是最佳选择。该策略在类 AntPathRequestMatcher 中实现,该类使用 Spring 的 AntPathMatcher 对连接的 servletPath 和 pathInfo 执行模式不区分大小写的匹配,忽略 queryString。

如果出于某种原因,需要更强大的匹配策略,可以使用正则表达式。策略实现是 RegexRequestMatcher。请参阅 JavaDoc 以获取更多有关此类的详细信息。

在实践中,我们建议你在服务层使用方法安全性,以控制对应用程序的访问,并且不要完全依赖于在 web 应用程序级别定义的安全约束的使用。URL 会改变,很难考虑应用程序可能支持的所有可能的 URL 以及如何操作请求。你应该试着用一些简单易懂的 Ant 路径来限制自己。始终尝试使用默认拒绝的方法,在所有的通配符(或)定义了最后一个和拒绝访问。

在服务层定义的安全性更加健壮,并且更难绕过,因此你应该始终利用 Spring Security 的方法安全性选项。

HttpFirewall 还通过拒绝 HTTP 响应头中的新行字符来防止 HTTP 响应拆分。

默认情况下使用 StrictHttpFirewall。此实现拒绝出现恶意的请求。如果对你的需求过于严格,那么你可以自定义拒绝什么类型的请求。但是,重要的是,你知道这会打开你的应用程序攻击。例如,如果希望利用 Spring MVC 的矩阵变量,可以在 XML 中使用以下配置:

<b:bean id="httpFirewall"
      class="org.springframework.security.web.firewall.StrictHttpFirewall"
      p:allowSemicolon="true"/>

<http-firewall ref="httpFirewall"/>

同样的事情可以通过暴露一个 StrictHttpFirewall bean 来实现。

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

StrictHttpFirewall 提供了有效 HTTP 方法的白名单,允许它们防止跨站点跟踪(XST)和 HTTP谓词篡改。默认的有效方法是 "DELETE"、"GET"、"HEAD"、"OPTIONS"、"PATCH"、"POST" 和 "PUT"。如果应用程序需要修改有效方法,可以配置自定义 StrictHttpFirewall bean。例如,下面只允许 HTTP "GET" 和 "POST" 方法:

<b:bean id="httpFirewall"
      class="org.springframework.security.web.firewall.StrictHttpFirewall"
      p:allowedHttpMethods="GET,HEAD"/>

<http-firewall ref="httpFirewall"/>

同样的事情可以通过暴露一个 StrictHttpFirewall bean 来实现。

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

如果您正在使用 new MockHttpServletRequest(),它当前创建一个 HTTP 方法作为一个空字符串 ""。这是一个无效的 HTTP 方法,将被 Spring Security 拒绝。你可以通过用 new MockHttpServletRequest("GET", "") 替换它来解决此问题。请参阅 SPR_16851 的一个问题,要求改进这一点。

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

14.5 与其它基于过滤器的框架一起使用

如果你使用的是其它基于过滤器的框架,那么你需要确保 Spring Security 过滤器是第一个。这使得 SecurityContextHolder 可以及时填充,供其它过滤器使用。例子是使用 SiteMesh 来装饰你的网页或者像 Wicket 这样的 web 框架,它使用一个过滤器来处理它的请求。

14.6 高级命名空间配置

正如我们在前面命名空间章节看到的,可以使用多个 http 元素来为不同的 URL 模式定义不同的安全配置。每个元素在内部 FilterChainProxy 和应该映射到它的 URL 模式中创建一个过滤器链。元素将按照声明的顺序添加,因此必须再次声明最特定的模式。对于与上面类似的情况,这里有另一个示例,其中应用程序既支持无状态 RESTful API,也支持用户使用表单登录的正常 web 应用程序。

<!-- Stateless RESTful service using Basic authentication -->
<http pattern="/restful/**" create-session="stateless">
<intercept-url pattern='/**' access="hasRole('REMOTE')" />
<http-basic />
</http>

<!-- Empty filter chain for the login page -->
<http pattern="/login.htm*" security="none"/>

<!-- Additional filter chain for normal users, matching all other requests -->
<http>
<intercept-url pattern='/**' access="hasRole('USER')" />
<form-login login-page='/login.htm' default-target-url="/home.htm"/>
<logout />
</http>


[6] 请注意,你需要在应用程序程序上下文 XML 文件包含安全命名空间以使用这个语法。使用 filter-chain-map 的旧语法仍然受到支持,但是对于构造函数参数注入不赞成。

[7] 可以使用 request-matcher-ref 属性而不是路径模式来指定 RequestMatcher 实例以进行更强大的匹配

[8] 当浏览器不支持 cookie 并且 jsessionid 参数在分号之后附加到 URL 时,你可能已经看到了这一点。然而,RFC 允许在 URL 的任何路径段中存在这些参数。

[9] 当请求离开 FilterChainProxy 时,原始值将返回,因此仍然适用于应用程序。

[10] 因此,例如,原始请求路径 /secure;hack=1/somefile.html;hack=2 将返回为 /secure/somefile.html。