博客 / 詳情

返回

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的必要性是什麼?我們一層一層逐步説明這個問題:

  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

user avatar xuezhongyu01 頭像
1 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.