Spring Security 6.x 过滤器链SecurityFilterChain是如何工作的
上一篇主要介绍了Spring Secuirty中的过滤器链SecurityFilterChain是如何配置的,那么在配置完成之后,SecurityFilterChain是如何在应用程序中调用各个Filter,从而起到安全防护的作用,本文主要围绕SecurityFilterChain的工作原理做详细的介绍。
一、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的工作原理就会变得比较好理解了:
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。
2. 委派FilterChainProxy:
上文提到在执行DelegatingFilterProxy的doFilter方法时,实际上都是交给FilterChainProxy来执行,它是由Spring容器托管的bean对象,通过下面WebSecurityConfiguration配置类源码可以看到,其中定义了一个名称为“springSecurityFilterChain”的Bean,而其中webSecurity#build方法返回的就是FilerChainProxy的实例,其构建过程和上一篇介绍的HttpSecurity类似,这里就不再展开。
@Bean(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. 执行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执行的过程。
@Override
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官方文档的图示,可以更好地理解整个执行流程