大多数Spring Security用户会在使用HTTP和servlet API的应用程序中使用该框架。在本章节,我们将了解Spring Security是如何为应用程序的Web层提供身份验证和访问控制功能。我们将查看名称空间的facade(外观设计模式),并查找实际上是哪些类和接口被组装了起来以提供Web层的安全。在某些情况下,有必要使用传统的bean配置来提供对配置的完全控制,因此我们还将了解如何在不使用命名空间的情况下直接配置这些类。
10.1 Security过滤器链
Spring Security的Web基础结构完全基于标准的servlet过滤器。它不在内部使用servlet或任何其他基于servlet的框架(如SpringMVC),因此它没有强关联到任何特定的Web技术。它处理HttpServletRequest和HttpServletResponse,而不关心这些请求是来自浏览器、Web服务客户机、HttpInvoker还是Ajax应用程序。
Spring Security在内部维护一个过滤器链,链中每个过滤器都有特定的职责,并且链中使用和不使用那些过滤器,取决于所请求的服务。过滤器的顺序很重要,因为它们之间存在依赖关系。如果您一直在使用命名空间进行配置,那么过滤器将自动为您配置,并且您不必显式定义任何SpringBean,但有时您希望完全控制安全过滤器链,要么因为您使用的是名称空间中不支持的功能,或者您正在使用自定义的类版本。
10.1.1 委托过滤器代理 DelegatingFilterProxy
当使用servlet过滤器时,你显然需要在web.xml中声明它们,否则它们将被servlet容器忽略。在Spring Security中,过滤器类也是在应用程序上下文中定义的SpringBean,因此能够利用Spring丰富的依赖注入工具和生命周期接口。Spring的 DelegatingFilterProxy 提供了web.xml和应用程序上下文之间的链接。
当使用 DelegatingFilterProxy 时,您将在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 所做的是将过滤器的方法委托给一个从Spring应用程序上下文获得的bean。这使得bean能够从SpringWeb应用程序上下文生命周期的支持和配置的灵活性中获益。bean必须实现 javax.servlet.Filter,并且它必须与filter name元素中的名称相同。有关详细信息,请阅读关于 DelegatingFilterProxy 的JavaDoc。
10.1.2 过滤链代理 FilterChainProxy
Spring Security的Web基础结构只能通过委托给 FilterChainProxy 实例来使用。 Security 过滤器不应该被本身使用。理论上,您可以声明在应用程序上下文文件中所需的每个SpringSecurity过滤器bean,并为每个过滤器向web.xml添加一个相应的 DelegatingFilterProxy 条目,确保它们的顺序正确,但这会很麻烦,如果您有很多过滤器,这杨还会很快使web.xml文件混乱不堪。FilterChainProxy 允许我们向web.xml添加一个条目,并全权处理应用程序上下文文件来管理我们的web security beans。它是使用 DelegatingFilterProxy 连接的,就像上面的例子一样,但是使用 filter-name 来设置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 用于方便设置应用程序中所需的安全筛选器链。它将特定的URL映射到由filters元素中指定的bean名称构建的过滤器列表,并将它们组合到类型为 SecurityFilterChain 的bean中。pattern属性采用Ant路径,最具体的URIs应该首先出现。在运行时,FilterChainProxy 将定位与当前Web请求匹配的第一个URI模式,由filters属性指定的filter bean列表将应用于该请求。过滤器将按照定义的顺序被调用,因此您可以完全控制应用于特定URL的过滤器链。
您可能已经注意到我们在过滤器链中声明了两个SecurityContextPersistenceFilter(ASC是allowSessionCreation的缩写,是SecurityContextPersistenceFilter 的一个属性)。由于Web服务永远不会在将来的请求中提供JSessionID,因此为此类用户代理创建 HttpSession 是浪费的。如果您有一个需要最大可伸缩性的大容量应用程序,我们建议您使用上面所示的方法。对于较小的应用程序,使用单个SecurityContextPersistenceFilter(其默认allowSessionCreation为true)可能就足够了。
注意,FilterChainProxy 不会在配置它的过滤器上调用标准的过滤器生命周期方法。我们建议您使用Spring的应用程序上下文生命周期接口作为替代方法,就像对任何其他SpringBean一样。
当我们研究如何使用命名空间配置来设置Web security 时,我们使用了一个名为“springSecurityFilterChain”的 DelegatingFilterProxy。现在应该可以看到这是由命名空间创建的 FilterChainProxy 的名称。
绕过过滤链
您可以使用属性 filters=“none” 作为提供过滤器bean列表的替代方法。这将完全忽略安全过滤器链中的请求模式。请注意,任何与此路径匹配的内容都不会应用任何身份验证或授权服务,并且可以自由访问。如果要在请求期间使用SecurityContext 的内容,那么它必须通过安全筛选器链传递。否则,SecurityContextHolder 将不会被填充,内容将为空。典型的例子就是:如果你的登录页面时未经过滤器链过滤的,那么页面上将不存在csrfToken的值,那么在系统中开启csrf(默认)但未提交csrfToken的情况下,登录认证将不通过。
10.1.3 过滤器排序
过滤器在链中定义的顺序非常重要。不管您实际使用的过滤器是什么,顺序应该如下:
ChannelProcessingFilter,因为它可能需要重定向到其他协议
SecurityContextPersistenceFilter,因此可以在Web请求开始时在SecurityContextHolder中设置SecurityContext,并且可以在Web请求结束时将对SecurityContext的任何更改复制到httpSession(准备用于下一个Web请求)
ConcurrentSessionFilter,因为它使用SecurityContextHolder功能,需要更新sessionRegistry以反映来自主体的持续请求
身份验证处理机制 - UsernamePasswordAuthenticationFilter、CasAuthenticationFilter、BasicAuthenticationFilter 等-以便可以修改SecurityContextHolder以包含有效的Authentication 请求令牌
SecurityContextHolderWareRequestFilter,如果您正在使用它将支持Spring Security 的HttpServletRequestWrapper 安装到servlet容器中
JaasApiIntegrationFilter,如果一个 JaasAuthenticationToken 在SecurityContextholder中,它将作为JaasAuthenticationToken的主体来处理FilterChain 。
RememberMeAuthenticationFilter,这样,如果没有早期的 authentication 处理机制更新了SecurityContextHolder,并且请求提供了一个允许执行“记住我”服务的cookie,那么将在那里放置一个合适的记住的 Authentication 对象。
AnonymousAuthenticationFilter,这样,如果没有早期的身份验证处理机制更新SecurityContextHolder,则会在那里放置一个匿名 Authentication 对象。
ExceptionTranslationFilter,捕获任何Spring Security 异常,以便返回HTTP错误响应或启动适当的AuthenticationEntryPoint 。
FilterSecurityInterceptor,用于保护 Web URIs并在拒绝访问时引发异常。
10.1.4 请求匹配和HttpFirewall
Spring Security 有几个方面(您定义的模式)针对传入的请求进行测试,以决定如何处理请求。当 FilterChainProxy 决定请求应该通过哪个过滤器链时,以及当 FilterSecurityInterceptor 决定采用哪种安全约束来作用于一个请求的时侯,就会发生这种情况。在根据您定义的模式进行测试时,了解机制是什么以及使用什么URL值是很重要的。
Servlet规范为 HttpServletRequest 定义了几个属性,这些属性可以通过getter方法访问,并且我们可能希望与之匹配。这些属性是contextPath、servletPath、pathInfo 和 queryString。Spring Security 只对保护应用程序中的路径感兴趣,因此忽略了contextPath 。不幸的是,servlet规范没有准确定义对于特定请求URI,servletPath 和 pathInfo 需要包含什么样的值。例如,URL的每个路径段可能包含参数,如RFC2396中所定义。规范没有明确说明这些是否应该包含在servletPath 和 pathInfo 的值中,并且不同的servlet容器之间的行为不同。当应用程序部署在不从这些值中删除路径参数的容器中时,就产生了危险,攻击者可以将它们添加到请求的URL中,以导致模式匹配意外成功或失败。传入URL中的其他变体也是可能的。例如,它可以包含路径遍历序列(如 /../)或多个正斜杠(//),这也可能导致模式匹配失败。一些容器在执行servlet映射之前将其规范化,但其他容器则没有这样做。为了防止此类问题,FilterChainProxy 使用HttpFirewall 策略检查和包装请求。默认情况下,未规范化的请求被自动拒绝,为了匹配的目的,路径参数和重复斜杠被删除。因此,必须使用 FilterChainProxy 来管理安全过滤器链。请注意,servletpath和pathinfo值是由容器解码的,因此您的应用程序不应该有任何包含分号的有效路径,因为为了进行匹配,这些部分将被删除。
如上所述,默认策略是使用Ant风格的路径进行匹配,这可能是大多数用户的最佳选择。该策略在类AntPathRequestMatcher 中实现,该类使用Spring的 antPathMatcher 对连接的servletPath和pathinfo执行不区分大小写的模式匹配,而忽略queryString。
如果出于某种原因,需要更强大的匹配策略,可以使用正则表达式。策略实现类是 RegexRequestMatcher。有关更多信息,请参阅此类的javadoc。
在实践中,建议您在服务层使用方法安全来控制对应用程序的访问,并且不要完全依赖于在Web应用程序级别定义的安全约束的使用。URLs会发生变化,很难考虑应用程序可能支持的所有可能的URL以及如何处理这些请求。您应该尝试并限制自己使用一些简单易懂的Ant路径。始终尝试使用“deny-by-default”方法,其中您具有最后定义的catch all通配符(/ or)并拒绝访问。
在服务层定义的安全性要强大得多,而且很难绕过,所以您应该始终利用Spring安全性的方法安全选项。
HttpFirewall 还通过拒绝HTTP响应头中的新行字符来防止HTTP响应拆分。
默认情况下使用StrictHttpFirewall 。此实现拒绝看似恶意的请求。如果它对您的需求太严格,那么您可以自定义拒绝哪些类型的请求。但是,重要的是要知道这样做可以使应用程序抵御攻击。例如,如果希望利用SpringMVC的矩阵变量,可以在XML中使用以下配置:
<b:beanid="httpFirewall"class="org.springframework.security.web.firewall.StrictHttpFirewall"p:allowSemicolon="true"/><http-firewallref="httpFirewall"/>
同样的事情可以通过暴露一个严格的防火墙防火墙来实现。
@Bean
publicStrictHttpFirewall httpFirewall() {
StrictHttpFirewall firewall =newStrictHttpFirewall();
firewall.setAllowSemicolon(true);
returnfirewall;
}
StrictHttpFirewall 提供了一个有效的HTTP方法白名单,这些方法可以防止跨站点跟踪(XST)和HTTP动词篡改。默认的有效方法是“DELETE”、“GET”、“HEAD”、“OPTIONS”、“PATCH”、“POST”和“PUT”。如果您的应用程序需要修改有效的方法,您可以配置一个自定义的StrictHttpFirewall bean。例如,以下仅允许http“GET”和“POST”方法:
<b:beanid="httpFirewall"class="org.springframework.security.web.firewall.StrictHttpFirewall"p:allowedHttpMethods="GET,HEAD"/>
<http-firewallref="httpFirewall"/>
同样的事情可以采用java配置的方式,通过暴露一个StrictHttpFirewall bean来实现。
@Bean
publicStrictHttpFirewall httpFirewall() {
StrictHttpFirewall firewall = newStrictHttpFirewall(); firewall.setAllowedHttpMethods(Arrays.asList("GET","POST"));
returnfirewall;
}
如果您正在使用新的mockhttpservletrequest(),它当前会创建一个HTTP方法作为空字符串“”。这是一个无效的HTTP方法,将被Spring安全性拒绝。您可以通过用新的mockhttpservletrequest(“get”,)替换它来解决这个问题。请参阅SPR U 16851了解请求改进此问题的问题。
如果必须允许任何HTTP方法(不推荐),则可以使用 StrictHttpFirewall.setUnsafeAllowAnyHttpMethod(true).。这将完全禁用HTTP方法的验证。
10.1.5 与其他基于过滤器的框架一起使用
如果正在使用的其他框架也是基于过滤器的,那么您需要确保请求首先经过Spring Security 过滤器。这使SecurityContextHolder 能够及时被填充以供其他筛选器使用。例如,使用SiteMesh来装饰网页,或者使用Wicket这样的Web框架来处理其请求。
10.1.6 高级命名空间配置
正如我们在前面的命名空间章节中看到的,可以使用多个 http 元素为不同的URL模式定义不同的安全配置。每个元素在内部 FilterChainProxy 和应该映射到它的url模式中创建一个过滤器链。元素将按声明的顺序添加,因此必须首先声明最具体的模式。这里是另一个例子,对于与上面类似的情况,应用程序既支持无状态的RESTfulAPI,也支持用户使用表单登录的普通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>