上一篇主要介绍了Spring Secuirty中的过滤器链SecurityFilterChain是如何配置的,那么在配置完成之后,SecurityFilterChain是如何在应用程序中调用各个Filter,从而起到安全防护的作用,本文主要围绕SecurityFilterChain的工作原理做详细的介绍。
因为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,先来思考一个问题:基于上面所介绍的Filter,我们自然会想到,定义一系列与安全相关的Filter,例如我们在上一篇提到的那些包括认证,鉴权等在内的Filter,然后只要把他们一个个注册到FilterChain中,就可以实现各种安全特性,看起来也并不需要Spring Security提供的SecuriyFilterChain,也正因如此,初学者经常会有一个疑问,就是明明加一个Filter就可以解决的事,为什么搞得这么复杂?
那么SecurityFilterChain的必要性是什么?我们一层一层逐步说明这个问题:
综上所述,通过DelegatingFilterProxy->FilterChainProxy->SecurityFilterChain这样的三层结构关系,使得SecurityFilterChain中的各个Filter被当成了一个整体,置于Servlet FilterChain之中,又能和其他的Filter独立开,不论我们如何配置SecurityFilterChain,都不会引起Servlet FilterChain的变更,这样的设计很好地遵循了开放封闭原则,即对Servlet Filter的修改是保持封闭的,而对Spring Security Filter的配置和扩展是保持开放的。
其实,我们在很多Spring的框架中,都可以见到这种设计,本质上来说,即通过添加一个中间层来达到解耦的目的,我们应该深入地理解这种设计,并学以致用。
讨论完SecurityFilterChain必要性,再来介绍SecurityFilterChain的工作原理就会变得比较好理解了:
在非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。
上文提到在执行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);
}
严格来说,最终执行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官方文档的图示,可以更好地理解整个执行流程
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。