一、Filter背景知识
因为Spring Security底层依赖Servlet的过滤器技术,所以先简单地回顾一下相关背景知识。
过滤器Filter是Servlet的标准组件,自Servlet 2.3版本引入,主要作用是在Servlet实例接受到请求之前,以及返回响应之后,这两个方向上进行动态拦截,这样就可以与Servlet主业务逻辑解耦,从而实现灵活性和可扩展性,利用这个特性可以实现很多功能,例如身份认证,统一编码,数据加密解密,审计日志等等。
Filter接口定义了3个方法:doFilter,init和destory,其中doFilter就是请求进入过滤器时需要执行的逻辑,伪代码实现如下
public class ExampleFilter implements Filter { … public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { doSomething(); chain.doFilter(request,response); } … }
其中FilterChain中维护了一个所有已注册的过滤器数组,它组成了真正的“过滤器链”,下面是FilterChain的实现类ApplicationFilterChain的部分源码:当请求到达Servlet容器时,就会创建出一个FilterChain实例,然后调用FilterChain#doFilter方法,这时会从数组中取出下一个过滤器,并调用Filter#doFilter方法,在方法末尾又会将请求继续交由FilterChain处理,如此往复,从而实现职责链模式的调用方式。
private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { // Call the next filter if there is one if (pos < n) { ApplicationFilterConfig filterConfig = filters[pos++]; try { Filter filter = filterConfig.getFilter(); ... if (Globals.IS_SECURITY_ENABLED) { // ... } else { filter.doFilter(request, response, this); } } catch (...) { ... } return; } // We fell off the end of the chain -- call the servlet instance try { ... // Use potentially wrapped request from this point if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse) && Globals.IS_SECURITY_ENABLED) { ... } else { servlet.service(request, response); } } catch (...) { ... } finally { ... } }
Filter实例可以在web.xml中注册,同时设置URL映射逻辑,当URL符合设置的规则时,便会进入该Filter,举个例子,在Spring Boot问世之前开发一个普通的Spring MVC应用时,经常会配置一个CharacterEncodingFilter,用于统一请求和响应的编码,以避免一些中文乱码的问题
<filter> <filter-name>characterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>characterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> <!-- 相当于拦截所有请求 --> </filter-mapping>
二、SecurityFilterChain的必要性
再回到SecurityFilterChain,先来思考一个问题:基于上面所介绍的Filter,我们自然会想到,定义一系列与安全相关的Filter,例如我们在上一篇提到的那些包括认证,鉴权等在内的Filter,然后只要把他们一个个注册到FilterChain中,就可以实现各种安全特性,看起来也并不需要Spring Security提供的SecuriyFilterChain,也正因如此,初学者经常会有一个疑问,就是明明加一个Filter就可以解决的事,为什么搞得这么复杂?
那么SecurityFilterChain的必要性是什么?我们一层一层逐步说明这个问题:
- 首先要解决的是如何在Filter中获取Spring容器中Bean对象,因为在Servlet容器中启动时,各个Filter的实例便会初始化并完成注册,此时Spring Bean对象还没有完成整个加载过程,不能直接注入,不过很容易想到,可以用一个“虚拟”的Filter在Servlet容器启动时先完成注册,然后在执行doFilter时,再获取对应的Spring Bean作为实际的Filter实例,执行具体的doFilter逻辑,这是一个典型的委派模式,Spring Security为此提供了一个名为DelegatingFilterProxy的类,下文再作详细介绍。
- 解决了Spring Bean容器与Servlet Filter整合的问题之后,我们是否可以将每一个Filter都通过DelegatingFilterProxy的模式添加到FilterChain中?试想一下,如果每个Spring Security的Filter都分别创建一个独立的委派类,那么通过ApplicationContext查找bean的代码就会反复出现,这在很大程度上违背了依赖注入的原则,也极大了增加了维护成本和开发成本,为了解决这个问题,在上述DelegateFilterProxy基础上,Spring Security又引入了一个代理类FilterChainProxy,它可以看作是Spring Security Filter的统一入口,此时,从Servlet的FIlterChain角度来看,整个Spring Security只定义了一个Filter,即DelegatingFilterProxy,而执行doFilter时则委派给了FilterChainProxy,这样就可以利用这个入口简化很多工作,例如官方文档中提到,可以在调试Spring Security功能时,将断点设置在这个入口,方便我们跟踪定位问题等等
- FilterChainProxy作为统一收口,同时也起到了打通SecurityFilterChain的桥梁作用,在调用doFilter方法时,实际上都交给某个SecurityFilterChain实例执行,到这里请求才算是进入了我们使用HttpSecurity配置的各个Filter,而在执行SecurityFilterChain的前后位置,又可以统一添加一些处理,例如添加Spring Security的防火墙HttpFirewall,用以防范某些特定类型的攻击
- 最后还有一点,Servlet Filter本身也存在一定的局限性,例如映射配置不够灵活,只能根据URL进行匹配,而SecurityFilterChain通过RequestMatcher接口实现了不同匹配逻辑及组合,大大丰富了匹配规则映射的能力
综上所述,通过DelegatingFilterProxy->FilterChainProxy->SecurityFilterChain这样的三层结构关系,使得SecurityFilterChain中的各个Filter被当成了一个整体,置于Servlet FilterChain之中,又能和其他的Filter独立开,不论我们如何配置SecurityFilterChain,都不会引起Servlet FilterChain的变更,这样的设计很好地遵循了开放封闭原则,即对Servlet Filter的修改是保持封闭的,而对Spring Security Filter的配置和扩展是保持开放的。
其实,我们在很多Spring的框架中,都可以见到这种设计,本质上来说,即通过添加一个中间层来达到解耦的目的,我们应该深入地理解这种设计,并学以致用。
三、SecuriyFilterChain的工作原理
讨论完SecurityFilterChain必要性,再来介绍SecurityFilterChain的工作原理就会变得比较好理解了:
3.1 注册DelegatingFilterProxy
在非Spring Boot环境可以通过web.xml进行注册,配置如下:
<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>
而在Spring Boot环境下,则是通过RegistrationBean的方式注册Servlet组件,具体实现类为DelegatingFilterProxyRegistrationBean,它由SecurityFilterAutoConfiguration配置类创建出来,并在Servlet容器启动的时候完成Filter的注册。
完成注册后,当Servlet容器启动时,FilterChain就包含了DelegatingFilterProxy这个Filter。
3.2 委派FilterChainProxy
上文提到在执行DelegatingFilterProxy的doFilter方法时,实际上都是交给FilterChainProxy来执行,它是由Spring容器托管的bean对象,通过下面WebSecurityConfiguration配置类源码可以看到,其中定义了一个名称为“springSecurityFilterChain”的Bean,而其中webSecurity#build方法返回的就是FilerChainProxy的实例,其构建过程和上一篇介绍的HttpSecurity类似,这里就不再展开。
name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) // "springSecurityFilterChain" (public Filter springSecurityFilterChain() throws Exception { ... return this.webSecurity.build(); }
委派过程比较简单,下面是DelegatingFilterProxy#doFilter方法的源码(可以忽略并发控制的代码),当请求进入doFilter之后,首先调用initDelegate方法,这里利用Spring的ApplicationContext#getBean方法获取名为“springSecurityFilterChain“的bean对象,即FilterChainProxy,然后调用其doFilter方法,这样就完成了委派调用。
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); } protected Filter initDelegate(WebApplicationContext wac) throws ServletException { String targetBeanName = getTargetBeanName(); // "springSecurityFilterChain" Assert.state(targetBeanName != null, "No target bean name set"); Filter delegate = wac.getBean(targetBeanName, Filter.class); if (isTargetFilterLifecycle()) { delegate.init(getFilterConfig()); } return delegate; } protected void invokeDelegate( Filter delegate, ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { delegate.doFilter(request, response, filterChain); }
3.3 执行SecurityFilterChain的过滤器链
严格来说,最终执行doFilter的并不是SecuritFilterChain,FilterChainProxy内部维护了一个SecurityFilterChain的List列表,在调用doFilter方法时,会根据SecurityFilterChain#match方法匹配的结果决定选择某一个SecurityFilterChain,然后取出该SecurityFilterChain所有的Filter,用其构造一个VirtualFilterChain,这才是实际意义上过滤器链执行的入口。
private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request); HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response); List<Filter> filters = getFilters(firewallRequest); // 重点关注这个方法,获取到某个SecurityFilterChain的所有Filter if (filters == null || filters.size() == 0) { ... firewallRequest.reset(); this.filterChainDecorator.decorate(chain).doFilter(firewallRequest, firewallResponse); return; } ... FilterChain reset = (req, res) -> { ... // Deactivate path stripping as we exit the security filter chain firewallRequest.reset(); chain.doFilter(req, res); }; // 装饰器模式,实际上返回了VirtualFilterChain的实例 this.filterChainDecorator.decorate(reset, filters).doFilter(firewallRequest, firewallResponse); } private List<Filter> getFilters(HttpServletRequest request) { int count = 0; for (SecurityFilterChain chain : this.filterChains) { ... if (chain.matches(request)) { return chain.getFilters(); } } return null; } public FilterChain decorate(FilterChain original, List<Filter> filters) { return new VirtualFilterChain(original, filters); }
VirtualFilterChain的实现也并不复杂,其doFilter方法源码如下,原理和Servlet的FilterChain的实现类ApplicationFilterChain基本类似,不过当所有Filter都执行完之后,它会交给originalChain继续执行,即回到Servlet的FilterChain。上文提到,如果要打断点debug,这里是一个比较好的位置,可以看到Spring Security中定义各个Filter执行的过程。
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { if (this.currentPosition == this.size) { this.originalChain.doFilter(request, response); return; } this.currentPosition++; Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1); if (logger.isTraceEnabled()) { String name = nextFilter.getClass().getSimpleName(); logger.trace(LogMessage.format("Invoking %s (%d/%d)", name, this.currentPosition, this.size)); } nextFilter.doFilter(request, response, this); }
四、总结
最后,再结合Spring Security官方文档的图示,可以更好地理解整个执行流程:
首先Spring Security注册了一个DelegatingFilterProxy的过滤器,置于Servlet FilterChain,而在实际执行时又委派给了FilterChainProxy,FilterChainProxy作为所有由Spring Security提供的Filter的统一代理入口,它的引入可以解决了在Filter中获取Spring托管的Bean对象,在执行其doFilter方法时,会调用SecurityFilterChain#match方法决定使用哪一个具体的SecurityFilterChain,不过最终在执行时,会使用所有该SecurityFilterChain中的Filter构建出一个VirtualFilterChain对象,这个是实际执行SecurityFilterChain的统一入口。