深刻理解 FilterChainProxy【源碼篇】

昨天有小夥伴加鬆哥微信,說他把鬆哥的 Spring Security 系列擼完了。。java

but 鬆哥這個系列還沒發完呢,在個人計劃中,Spring Security 系列目前應該能更新一半,還剩一半,雖然有的小夥伴可能以爲好像已經沒啥了,其實還有不少東西。。。web

鬆哥最近也是特別忙,Security 更新慢下來了,可是秉持前面說的,要學就成系列的學,要學就學透徹,這個系列我還會繼續更下去。瀏覽器

今天咱們就來聊一聊 Spring Security 系列中的 FilterChainProxy。安全

這是一個很是重要的代理對象。微信

1. FilterChainProxy

咱們先來回顧一下前面文章講的:ide

在一個 Web 項目中,請求流程大概以下圖所示:工具

請求從客戶端發起(例如瀏覽器),而後穿過層層 Filter,最終來到 Servlet 上,被 Servlet 所處理。源碼分析

上圖中的 Filter 咱們能夠稱之爲 Web Filter,Spring Security 中的 Filter 咱們能夠稱之爲 Security Filter,它們之間的關係以下圖:ui

能夠看到,Spring Security Filter 並非直接嵌入到 Web Filter 中的,而是經過 FilterChainProxy 來統一管理 Spring Security Filter,FilterChainProxy 自己則經過 Spring 提供的 DelegatingFilterProxy 代理過濾器嵌入到 Web Filter 之中。this

DelegatingFilterProxy 不少小夥伴應該比較熟悉,在 Spring 中手工整合 Spring Session、Shiro 等工具時都離不開它,如今用了 Spring Boot,不少事情 Spring Boot 幫咱們作了,因此有時候會感受 DelegatingFilterProxy 的存在感有所下降,實際上它一直都在。

FilterChainProxy 中能夠存在多個過濾器鏈,以下圖:

能夠看到,當請求到達 FilterChainProxy 以後,FilterChainProxy 會根據請求的路徑,將請求轉發到不一樣的 Spring Security Filters 上面去,不一樣的 Spring Security Filters 對應了不一樣的過濾器,也就是不一樣的請求將通過不一樣的過濾器。

這是 FilterChainProxy 的一個大體功能,今天咱們就從源碼上理解 FilterChainProxy 中這些功能究竟是怎麼實現的。

2. 源碼分析

先把 FilterChainProxy 源碼亮出來,這個源碼比較上,咱們一部分一部分來,先從它聲明的全局屬性上開始:

private final static String FILTER_APPLIED = FilterChainProxy.class.getName().concat(
        ".APPLIED");
private List<SecurityFilterChain> filterChains;
private FilterChainValidator filterChainValidator = new NullFilterChainValidator();
private HttpFirewall firewall = new StrictHttpFirewall();
  • FILTER_APPLIED 變量是一個標記,用來標記過濾器是否已經執行過了。這個標記在 Spring Security 中很常見,鬆哥這裏就很少說了。
  • filterChains 是過濾器鏈,注意,這個是過濾器鏈,而不是一個個的過濾器,在【Spring Security 居然能夠同時存在多個過濾器鏈?】一文中,鬆哥教過你們如何配置多個過濾器鏈,配置的多個過濾器鏈就保存在 filterChains 變量中,也就是,若是你有一個過濾器鏈,這個集合中就保存一條記錄,你有兩個過濾器鏈,這個記錄中就保存兩條記錄,每一條記錄又對應了過濾器鏈中的一個個過濾器。
  • filterChainValidator 是 FilterChainProxy 配置完成後的校驗方法,默認使用的 NullFilterChainValidator 實際上對應了一個空方法,也就是不作任何校驗。
  • firewall 咱們在前面的文章中也介紹過(Spring Security 自帶防火牆!你都不知道本身的系統有多安全!),這裏就再也不贅述。

接下來咱們來看一個過濾器中最重要的 doFilter 方法:

@Override
public void doFilter(ServletRequest request, ServletResponse response,
        FilterChain chain) throws IOException, ServletException {
    boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
    if (clearContext) {
        try {
            request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
            doFilterInternal(request, response, chain);
        }
        finally {
            SecurityContextHolder.clearContext();
            request.removeAttribute(FILTER_APPLIED);
        }
    }
    else {
        doFilterInternal(request, response, chain);
    }
}

在 doFilter 方法中,正常來講,clearContext 參數每次都是 true,因而每次都先給 request 標記上 FILTER_APPLIED 屬性,而後執行 doFilterInternal 方法去走過濾器,執行完畢後,最後在 finally 代碼塊中清除 SecurityContextHolder 中保存的用戶信息,同時移除 request 中的標記。

按着這個順序,咱們來看 doFilterInternal 方法:

private void doFilterInternal(ServletRequest request, ServletResponse response,
        FilterChain chain) throws IOException, ServletException {
    FirewalledRequest fwRequest = firewall
            .getFirewalledRequest((HttpServletRequest) request);
    HttpServletResponse fwResponse = firewall
            .getFirewalledResponse((HttpServletResponse) response);
    List<Filter> filters = getFilters(fwRequest);
    if (filters == null || filters.size() == 0) {
        if (logger.isDebugEnabled()) {
            logger.debug(UrlUtils.buildRequestUrl(fwRequest)
                    + (filters == null ? " has no matching filters"
                            : " has an empty filter list"));
        }
        fwRequest.reset();
        chain.doFilter(fwRequest, fwResponse);
        return;
    }
    VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
    vfc.doFilter(fwRequest, fwResponse);
}
private List<Filter> getFilters(HttpServletRequest request) {
    for (SecurityFilterChain chain : filterChains) {
        if (chain.matches(request)) {
            return chain.getFilters();
        }
    }
    return null;
}

doFilterInternal 方法就比較重要了:

  1. 首先將請求封裝爲一個 FirewalledRequest 對象,在這個封裝的過程當中,也會判斷請求是否合法。具體參考鬆哥以前的 Spring Security 自帶防火牆!你都不知道本身的系統有多安全! 一文。
  2. 對響應進行封裝。
  3. 調用 getFilters 方法找到過濾器鏈。該方法就是根據當前的請求,從 filterChains 中找到對應的過濾器鏈,而後由該過濾器鏈去處理請求,具體能夠參考 Spring Security 居然能夠同時存在多個過濾器鏈? 一文。
  4. 若是找出來的 filters 爲 null,或者集合中沒有元素,那就是說明當前請求不須要通過過濾器。直接執行 chain.doFilter ,這個就又回到原生過濾器中去了。那麼何時會發生這種狀況呢?那就是針對項目中的靜態資源,若是咱們配置了資源放行,如 web.ignoring().antMatchers("/hello");,那麼當你請求 /hello 接口時就會走到這裏來,也就是說這個不通過 Spring Security Filter。這個鬆哥以前也專門寫文章介紹過:Spring Security 兩種資源放行策略,千萬別用錯了!
  5. 若是查詢到的 filters 中是有值的,那麼這個 filters 集合中存放的就是咱們要通過的過濾器鏈了。此時它會構造出一個虛擬的過濾器鏈 VirtualFilterChain 出來,並執行其中的 doFilter 方法。

那麼接下來咱們就來看看 VirtualFilterChain:

private static class VirtualFilterChain implements FilterChain {
    private final FilterChain originalChain;
    private final List<Filter> additionalFilters;
    private final FirewalledRequest firewalledRequest;
    private final int size;
    private int currentPosition = 0;
    private VirtualFilterChain(FirewalledRequest firewalledRequest,
            FilterChain chain, List<Filter> additionalFilters) {
        this.originalChain = chain;
        this.additionalFilters = additionalFilters;
        this.size = additionalFilters.size();
        this.firewalledRequest = firewalledRequest;
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response)
            throws IOException, ServletException {
        if (currentPosition == size) {
            if (logger.isDebugEnabled()) {
                logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
                        + " reached end of additional filter chain; proceeding with original chain");
            }
            // Deactivate path stripping as we exit the security filter chain
            this.firewalledRequest.reset();
            originalChain.doFilter(request, response);
        }
        else {
            currentPosition++;
            Filter nextFilter = additionalFilters.get(currentPosition - 1);
            if (logger.isDebugEnabled()) {
                logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
                        + " at position " + currentPosition + " of " + size
                        + " in additional filter chain; firing Filter: '"
                        + nextFilter.getClass().getSimpleName() + "'");
            }
            nextFilter.doFilter(request, response, this);
        }
    }
}
  1. VirtualFilterChain 類中首先聲明瞭 5 個全局屬性,originalChain 表示原生的過濾器鏈,也就是 Web Filter;additionalFilters 表示 Spring Security 中的過濾器鏈;firewalledRequest 表示當前請求;size 表示過濾器鏈中過濾器的個數;currentPosition 則是過濾器鏈遍歷時候的下標。
  2. doFilter 方法就是 Spring Security 中過濾器挨個執行的過程,若是 currentPosition == size,表示過濾器鏈已經執行完畢,此時經過調用 originalChain.doFilter 進入到原生過濾鏈方法中,同時也退出了 Spring Security 過濾器鏈。不然就從 additionalFilters 取出 Spring Security 過濾器鏈中的一個個過濾器,挨個調用 doFilter 方法。

最後,FilterChainProxy 中還定義了 FilterChainValidator 接口及其實現:

public interface FilterChainValidator {
    void validate(FilterChainProxy filterChainProxy);
}
private static class NullFilterChainValidator implements FilterChainValidator {
    @Override
    public void validate(FilterChainProxy filterChainProxy) {
    }
}

實際上這個實現並未作任何事情。

這就是 FilterChainProxy 中的整個邏輯。

3. 小結

其實本文中的不少知識點鬆哥在以前的文章中都和你們介紹過,例如配置多個過濾器鏈、StrictHttpFireWall、兩種資源放行方式等等,可是以前主要是和你們分享用法,沒有去細究他的原理,今天的文章,咱們經過源碼把這個問題捋了一遍,相信你們對於這些功能細節也更清晰了。

好啦,感興趣的小夥伴記得點個在看鼓勵下鬆哥哦~

相關文章
相關標籤/搜索