深入理解 WebSecurityConfigurerAdapter【原始碼篇】

江南一點雨發表於2020-07-29

我們繼續來擼 Spring Security 原始碼,今天來擼一個非常重要的 WebSecurityConfigurerAdapter。

我們的自定義都是繼承自 WebSecurityConfigurerAdapter 來實現的,但是對於 WebSecurityConfigurerAdapter 內部的工作原理,配置原理,很多小夥伴可能都還不太熟悉,因此我們今天就來捋一捋。

我們先來看一張 WebSecurityConfigurerAdapter 的繼承關係圖:

在這層繼承關係中,有兩個非常重要的類:

  • SecurityBuilder
  • SecurityConfigurer

這兩個類鬆哥在之前的文章中都和大家分享過了,具體參考:

所以關於這兩個類的介紹以及作用,鬆哥這裡就不贅述了。我們們直接從 WebSecurityConfigurer 開始看起。

1.WebSecurityConfigurer

WebSecurityConfigurer 其實是一個空介面,但是它裡邊約束了一些泛型,如下:

public interface WebSecurityConfigurer<T extends SecurityBuilder<Filter>> extends
        SecurityConfigurer<Filter, T> {

}

這裡邊的泛型很關鍵,這關乎到 WebSecurityConfigurer 的目的是啥!

  1. SecurityBuilder 中的泛型 Filter,表示 SecurityBuilder 最終的目的是為了構建一個 Filter 物件出來。
  2. SecurityConfigurer 中兩個泛型,第一個表示的含義也是 SecurityBuilder 最終構建的物件。

同時這裡還定義了新的泛型 T,T 需要繼承自 SecurityBuilder<Filter>,根據 WebSecurityConfigurerAdapter 中的定義,我們可以知道,T 就是 WebSecurity,我們也大概能猜出 WebSecurity 就是 SecurityBuilder<Filter> 的子類。

所以 WebSecurityConfigurer 的目的我們可以理解為就是為了配置 WebSecurity。

2.WebSecurity

我們來看下 WebSecurity 的定義:

public final class WebSecurity extends
        AbstractConfiguredSecurityBuilder<Filter, WebSecurity> implements
        SecurityBuilder<Filter>, ApplicationContextAware {
}

沒錯,確實是這樣!WebSecurity 繼承自 AbstractConfiguredSecurityBuilder<Filter, WebSecurity> 同時實現了 SecurityBuilder<Filter> 介面。

WebSecurity 的這些介面和繼承類,鬆哥在前面的原始碼分析中都和大家介紹過了,可能有的小夥伴忘記了,我再來和大家複習一下。

AbstractConfiguredSecurityBuilder

首先 AbstractConfiguredSecurityBuilder 中定義了一個列舉類,將整個構建過程分為 5 種狀態,也可以理解為構建過程生命週期的五個階段,如下:

private enum BuildState {
    UNBUILT(0),
    INITIALIZING(1),
    CONFIGURING(2),
    BUILDING(3),
    BUILT(4);
    private final int order;
    BuildState(int order) {
        this.order = order;
    }
    public boolean isInitializing() {
        return INITIALIZING.order == order;
    }
    public boolean isConfigured() {
        return order >= CONFIGURING.order;
    }
}

五種狀態分別是 UNBUILT、INITIALIZING、CONFIGURING、BUILDING 以及 BUILT。另外還提供了兩個判斷方法,isInitializing 判斷是否正在初始化,isConfigured 表示是否已經配置完畢。

AbstractConfiguredSecurityBuilder 中的方法比較多,鬆哥在這裡列出來兩個關鍵的方法和大家分析:

private <C extends SecurityConfigurer<O, B>> void add(C configurer) {
    Assert.notNull(configurer, "configurer cannot be null");
    Class<? extends SecurityConfigurer<O, B>> clazz = (Class<? extends SecurityConfigurer<O, B>>) configurer
            .getClass();
    synchronized (configurers) {
        if (buildState.isConfigured()) {
            throw new IllegalStateException("Cannot apply " + configurer
                    + " to already built object");
        }
        List<SecurityConfigurer<O, B>> configs = allowConfigurersOfSameType ? this.configurers
                .get(clazz) : null;
        if (configs == null) {
            configs = new ArrayList<>(1);
        }
        configs.add(configurer);
        this.configurers.put(clazz, configs);
        if (buildState.isInitializing()) {
            this.configurersAddedInInitializing.add(configurer);
        }
    }
}
private Collection<SecurityConfigurer<O, B>> getConfigurers() {
    List<SecurityConfigurer<O, B>> result = new ArrayList<>();
    for (List<SecurityConfigurer<O, B>> configs : this.configurers.values()) {
        result.addAll(configs);
    }
    return result;
}

第一個就是這個 add 方法,這相當於是在收集所有的配置類。將所有的 xxxConfigure 收集起來儲存到 configurers 中,將來再統一初始化並配置,configurers 本身是一個 LinkedHashMap ,key 是配置類的 class,value 是一個集合,集合裡邊放著 xxxConfigure 配置類。當需要對這些配置類進行集中配置的時候,會通過 getConfigurers 方法獲取配置類,這個獲取過程就是把 LinkedHashMap 中的 value 拿出來,放到一個集合中返回。

另一個方法就是 doBuild 方法。

@Override
protected final O doBuild() throws Exception {
    synchronized (configurers) {
        buildState = BuildState.INITIALIZING;
        beforeInit();
        init();
        buildState = BuildState.CONFIGURING;
        beforeConfigure();
        configure();
        buildState = BuildState.BUILDING;
        O result = performBuild();
        buildState = BuildState.BUILT;
        return result;
    }
}
private void init() throws Exception {
    Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();
    for (SecurityConfigurer<O, B> configurer : configurers) {
        configurer.init((B) this);
    }
    for (SecurityConfigurer<O, B> configurer : configurersAddedInInitializing) {
        configurer.init((B) this);
    }
}
private void configure() throws Exception {
    Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();
    for (SecurityConfigurer<O, B> configurer : configurers) {
        configurer.configure((B) this);
    }
}

在 AbstractSecurityBuilder 類中,過濾器的構建被轉移到 doBuild 方法上面了,不過在 AbstractSecurityBuilder 中只是定義了抽象的 doBuild 方法,具體的實現在 AbstractConfiguredSecurityBuilder。

doBuild 方法就是一邊更新狀態,進行進行初始化。

beforeInit 是一個預留方法,沒有任何實現。

init 方法就是找到所有的 xxxConfigure,挨個呼叫其 init 方法進行初始化。

beforeConfigure 是一個預留方法,沒有任何實現。

configure 方法就是找到所有的 xxxConfigure,挨個呼叫其 configure 方法進行配置。

最後則是 performBuild 方法,是真正的過濾器鏈構建方法,但是在 AbstractConfiguredSecurityBuilder 中 performBuild 方法只是一個抽象方法,具體的實現在它的子類中,也就是 WebSecurityConfigurer。

SecurityBuilder<Filter>

SecurityBuilder 就是用來構建過濾器鏈的,在 HttpSecurity 實現 SecurityBuilder 時,傳入的泛型就是 DefaultSecurityFilterChain,所以 SecurityBuilder#build 方法的功能很明確,就是用來構建一個過濾器鏈出來,但是那個過濾器鏈是 Spring Security 中的。在 WebSecurityConfigurerAdapter 中定義的泛型是 SecurityBuilder<Filter>,所以最終構建的是一個普通 Filter,其實就是 FilterChainProxy,關於 FilterChainProxy ,大家可以參考深入理解 FilterChainProxy【原始碼篇】

WebSecurity

WebSecurity 的核心邏輯集中在 performBuild 構建方法上,我們一起來看下:

@Override
protected Filter performBuild() throws Exception {
    Assert.state(
            !securityFilterChainBuilders.isEmpty(),
            () -> "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. "
                    + "Typically this done by adding a @Configuration that extends WebSecurityConfigurerAdapter. "
                    + "More advanced users can invoke "
                    + WebSecurity.class.getSimpleName()
                    + ".addSecurityFilterChainBuilder directly");
    int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();
    List<SecurityFilterChain> securityFilterChains = new ArrayList<>(
            chainSize);
    for (RequestMatcher ignoredRequest : ignoredRequests) {
        securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
    }
    for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {
        securityFilterChains.add(securityFilterChainBuilder.build());
    }
    FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
    if (httpFirewall != null) {
        filterChainProxy.setFirewall(httpFirewall);
    }
    filterChainProxy.afterPropertiesSet();
    Filter result = filterChainProxy;
    if (debugEnabled) {
        logger.warn("\n\n"
                + "********************************************************************\n"
                + "**********        Security debugging is enabled.       *************\n"
                + "**********    This may include sensitive information.  *************\n"
                + "**********      Do not use in a production system!     *************\n"
                + "********************************************************************\n\n");
        result = new DebugFilter(filterChainProxy);
    }
    postBuildAction.run();
    return result;
}

先來說一句,這裡的 performBuild 方法只有一個功能,那就是構建 FilterChainProxy,如果你還不瞭解什麼是 FilterChainProxy,可以參考鬆哥之前的介紹:深入理解 FilterChainProxy【原始碼篇】

把握住了這條主線,我們再來看方法的實現就很容易了。

  1. 首先統計過濾器鏈的總條數,總條數包括兩個方面,一個是 ignoredRequests,這是忽略的請求,通過 WebSecurity 配置的忽略請求,鬆哥之前介紹過,參見:Spring Security 兩種資源放行策略,千萬別用錯了!,另一個則是 securityFilterChainBuilders,也就是我們通過 HttpSecurity 配置的過濾器鏈,有幾個就算幾個。
  2. 建立 securityFilterChains 集合,並且遍歷上面提到的兩種型別的過濾器鏈,並將過濾器鏈放入 securityFilterChains 集合中。
  3. 我在深入理解 HttpSecurity【原始碼篇】一文中介紹過,HttpSecurity 構建出來的過濾器鏈物件就是 DefaultSecurityFilterChain,所以可以直接將 build 結果放入 securityFilterChains 中,而 ignoredRequests 中儲存的則需要重構一下才可以存入 securityFilterChains。
  4. securityFilterChains 中有資料之後,接下來建立一個 FilterChainProxy。
  5. 給新建的 FilterChainProxy 配置上防火牆,防火牆的介紹參考鬆哥之前的:Spring Security 自帶防火牆!你都不知道自己的系統有多安全!
  6. 最後我們返回的就是 FilterChainProxy 的例項。

從這段分析中,我們可以看出來 WebSecurity 和 HttpSecurity 的區別:

  1. HttpSecurity 目的是構建過濾器鏈,一個 HttpSecurity 物件構建一條過濾器鏈,一個過濾器鏈中有 N 個過濾器,HttpSecurity 所做的事情實際上就是在配置這 N 個過濾器。
  2. WebSecurity 目的是構建 FilterChainProxy,一個 FilterChainProxy 中包含有多個過濾器鏈和一個 Firewall。

這就是 WebSecurity 的主要作用,核心方法是 performBuild,其他方法都比較簡單,鬆哥就不一一解釋了。

3.WebSecurityConfigurerAdapter

最後我們再來看 WebSecurityConfigurerAdapter,由於 WebSecurityConfigurer 只是一個空介面,WebSecurityConfigurerAdapter 就是針對這個空介面提供一個具體的實現,最終目的還是為了方便你配置 WebSecurity。

WebSecurityConfigurerAdapter 中的方法比較多,但是根據我們前面的分析,提綱挈領的方法就兩個,一個是 init,還有一個 configure(WebSecurity web),其他方法都是為這兩個方法服務的。那我們就來看下這兩個方法:

先看 init 方法:

public void init(final WebSecurity web) throws Exception {
    final HttpSecurity http = getHttp();
    web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
        FilterSecurityInterceptor securityInterceptor = http
                .getSharedObject(FilterSecurityInterceptor.class);
        web.securityInterceptor(securityInterceptor);
    });
}
protected final HttpSecurity getHttp() throws Exception {
    if (http != null) {
        return http;
    }
    AuthenticationEventPublisher eventPublisher = getAuthenticationEventPublisher();
    localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);
    AuthenticationManager authenticationManager = authenticationManager();
    authenticationBuilder.parentAuthenticationManager(authenticationManager);
    Map<Class<?>, Object> sharedObjects = createSharedObjects();
    http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
            sharedObjects);
    if (!disableDefaults) {
        // @formatter:off
        http
            .csrf().and()
            .addFilter(new WebAsyncManagerIntegrationFilter())
            .exceptionHandling().and()
            .headers().and()
            .sessionManagement().and()
            .securityContext().and()
            .requestCache().and()
            .anonymous().and()
            .servletApi().and()
            .apply(new DefaultLoginPageConfigurer<>()).and()
            .logout();
        // @formatter:on
        ClassLoader classLoader = this.context.getClassLoader();
        List<AbstractHttpConfigurer> defaultHttpConfigurers =
                SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);
        for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
            http.apply(configurer);
        }
    }
    configure(http);
    return http;
}
protected void configure(HttpSecurity http) throws Exception {
    logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
    http
        .authorizeRequests()
            .anyRequest().authenticated()
            .and()
        .formLogin().and()
        .httpBasic();
}

init 方法可以算是這裡的入口方法了:首先呼叫 getHttp 方法進行 HttpSecurity 的初始化。HttpSecurity 的初始化,實際上就是配置了一堆預設的過濾器,配置完成後,最終還呼叫了 configure(http) 方法,該方法又配置了一些攔截器,不過在實際開發中,我們經常會重寫 configure(http) 方法,在鬆哥本系列前面的文章中,configure(http) 方法幾乎都有重寫。HttpSecurity 配置完成後,再將 HttpSecurity 放入 WebSecurity 中,儲存在 WebSecurity 的 securityFilterChainBuilders 集合裡,具體參見:深入理解 HttpSecurity【原始碼篇】

configure(WebSecurity web) 方法實際上是一個空方法,我們在實際開發中可能會重寫該方法(參見 Spring Security 兩種資源放行策略,千萬別用錯了! 一文):

public void configure(WebSecurity web) throws Exception {
}

4.小結

這便是 WebSecurityConfigurerAdapter,整體上來說並不難,但是要和鬆哥前面幾篇原始碼分析文章一起看,理解會更加深刻一些。

傳送門:

  1. 深入理解 FilterChainProxy【原始碼篇】
  2. 深入理解 SecurityConfigurer 【原始碼篇】
  3. 深入理解 HttpSecurity【原始碼篇】
  4. 深入理解 AuthenticationManagerBuilder 【原始碼篇】

好啦,小夥伴們要是有收穫,記得點個在看鼓勵下鬆哥哦~

相關文章