头图

上一篇主要介绍了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的必要性是什么?我们一层一层逐步说明这个问题:

  1. 首先要解决的是如何在Filter中获取Spring容器中Bean对象,因为在Servlet容器中启动时,各个Filter的实例便会初始化并完成注册,此时Spring Bean对象还没有完成整个加载过程,不能直接注入,不过很容易想到,可以用一个“虚拟”的Filter在Servlet容器启动时先完成注册,然后在执行doFilter时,再获取对应的Spring Bean作为实际的Filter实例,执行具体的doFilter逻辑,这是一个典型的委派模式,Spring Security为此提供了一个名为DelegatingFilterProxy的类,下文再作详细介绍。
  2. 解决了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功能时,将断点设置在这个入口,方便我们跟踪定位问题等等
  3. FilterChainProxy作为统一收口,同时也起到了打通SecurityFilterChain的桥梁作用,在调用doFilter方法时,实际上都交给某个SecurityFilterChain实例执行,到这里请求才算是进入了我们使用HttpSecurity配置的各个Filter,而在执行SecurityFilterChain的前后位置,又可以统一添加一些处理,例如添加Spring Security的防火墙HttpFirewall,用以防范某些特定类型的攻击
  4. 最后还有一点,Servlet Filter本身也存在一定的局限性,例如映射配置不够灵活,只能根据URL进行匹配,而SecurityFilterChain通过RequestMatcher接口实现了不同匹配逻辑及组合,大大丰富了匹配规则映射的能力

综上所述,通过DelegatingFilterProxy->FilterChainProxy->SecurityFilterChain这样的三层结构关系,使得SecurityFilterChain中的各个Filter被当成了一个整体,置于Servlet FilterChain之中,又能和其他的Filter独立开,不论我们如何配置SecurityFilterChain,都不会引起Servlet FilterChain的变更,这样的设计很好地遵循了开放封闭原则,即对Servlet Filter的修改是保持封闭的,而对Spring Security Filter的配置和扩展是保持开放的。
其实,我们在很多Spring的框架中,都可以见到这种设计,本质上来说,即通过添加一个中间层来达到解耦的目的,我们应该深入地理解这种设计,并学以致用。

image.png

三、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官方文档的图示,可以更好地理解整个执行流程

image.png


fullstackyang
186 声望14 粉丝

以艺术家为成长目标的程序员