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.Filter
。
DelegatingFilterProxy
继承 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 安全。
该类同样继承自 GenericFilterBean
,Filter
核心实现如下:
@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。- 身份验证处理机制 -
UsernamePasswordAuthenticationFilter
,CasAuthenticationFilter
,BasicAuthenticationFilter
等 - 以便可以修改 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 资源的安全性,它需要一个 AuthenticationManager
和 AccessDecisionManager
的引用。它还提供了适用于不同 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
。安全拦截器将在调用堆栈的下方抛出适当的 AuthenticationException
或 AccessDeniedException
,触发入口点的 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
来处理每个身份验证请求。身份验证成功或身份验证失败后的目标分别由 AuthenticationSuccessHandler
和 AuthenticationFailureHandler
策略接口控制。分别的,过滤器具有这些属性以便您可以完全自定义行为。提供了一些标准实现,如 SimpleUrlAuthenticationSuccessHandler
, SavedRequestAwareAuthenticationSuccessHandler
, SimpleUrlAuthenticationFailureHandler
, ExceptionMappingAuthenticationFailureHandler
and DelegatingAuthenticationFailureHandler
。查看这些类的 Javadoc 以及 AbstractAuthenticationProcessingFilter
,以了解它们的工作原理和支持的功能。
如果认证成功后,创建的 Authentication
对象将被放入 SecurityContextHolder
中。然后将调用配置的 AuthenticationSuccessHandler
,以将用户重定向或转发到适当的目标。默认情况下,使用 SavedRequestAwareAuthenticationSuccessHandler
,这意味着在要求用户登录之前,用户将被重定向到他们请求的原始目标。
如果身份验证失败,将调用配置的 AuthenticationFailureHandler
。
请求匹配和 HttpFirewall
Servlet 规约为 HttpServletRequest 定义了一些属性,我们可能希望与之匹配来验证安全。这些是 contextPath
,servletPath
,pathinfo
和 queryString
。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:
Example | Matches: |
---|---|
com/t?st.jsp | com/test.jsp、com/tast.jsp、com/txst.jsp |
com/*.jsp | com 目录下的所有 .jsp 文件 |
com/**/test.jsp | com 路径下的所有 test.jsp 文件 |
org/springframework/**/*.jsp | org/springframework 路径下的所有 .jsp 文件 |
org/**/servlet/bla.jsp | org/springframework/servlet/bla.jsp |
also: | org/springframework/testing/servlet/bla.jsp |
also: | org/servlet/bla.jsp |
com/{filename:\w+}.jsp | com/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 请求类型的验证。