前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring Security 6.x 过滤器链SecurityFilterChain是如何工作的

Spring Security 6.x 过滤器链SecurityFilterChain是如何工作的

原创
作者头像
fullstackyang
修改2024-06-02 00:21:41
1830
修改2024-06-02 00:21:41
举报

上一篇主要介绍了Spring Secuirty中的过滤器链SecurityFilterChain是如何配置的,那么在配置完成之后,SecurityFilterChain是如何在应用程序中调用各个Filter,从而起到安全防护的作用,本文主要围绕SecurityFilterChain的工作原理做详细的介绍。

一、Filter背景知识

因为Spring Security底层依赖Servlet的过滤器技术,所以先简单地回顾一下相关背景知识。

过滤器Filter是Servlet的标准组件,自Servlet 2.3版本引入,主要作用是在Servlet实例接受到请求之前,以及返回响应之后,这两个方向上进行动态拦截,这样就可以与Servlet主业务逻辑解耦,从而实现灵活性和可扩展性,利用这个特性可以实现很多功能,例如身份认证,统一编码,数据加密解密,审计日志等等。

Filter接口定义了3个方法:doFilter,init和destory,其中doFilter就是请求进入过滤器时需要执行的逻辑,伪代码实现如下

代码语言:javascript
复制
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处理,如此往复,从而实现职责链模式的调用方式。

代码语言:javascript
复制
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,用于统一请求和响应的编码,以避免一些中文乱码的问题

代码语言:javascript
复制
<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的框架中,都可以见到这种设计,本质上来说,即通过添加一个中间层来达到解耦的目的,我们应该深入地理解这种设计,并学以致用。

三、SecuriyFilterChain的工作原理

讨论完SecurityFilterChain必要性,再来介绍SecurityFilterChain的工作原理就会变得比较好理解了:

3.1 注册DelegatingFilterProxy

在非Spring Boot环境可以通过web.xml进行注册,配置如下:

代码语言:javascript
复制
<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类似,这里就不再展开。

代码语言:javascript
复制
@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方法,这样就完成了委派调用。

代码语言:javascript
复制
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,这才是实际意义上过滤器链执行的入口。

代码语言:javascript
复制
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执行的过程。

代码语言:javascript
复制
@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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、Filter背景知识
  • 二、SecurityFilterChain的必要性
  • 三、SecuriyFilterChain的工作原理
    • 3.1 注册DelegatingFilterProxy
      • 3.2 委派FilterChainProxy
        • 3.3 执行SecurityFilterChain的过滤器链
        相关产品与服务
        容器服务
        腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档