深入理解 WebSecurityConfigurerAdapter【源碼篇】
松哥原創的 Spring Boot 視頻教程已經殺青,感興趣的小夥伴戳這裏--> Spring Boot+Vue+微人事視頻教程
我們繼續來擼 Spring Security 源碼,今天來擼一個非常重要的 WebSecurityConfigurerAdapter。
我們的自定義都是繼承自 WebSecurityConfigurerAdapter 來實現的,但是對於 WebSecurityConfigurerAdapter 內部的工作原理,配置原理,很多小夥伴可能都還不太熟悉,因此我們今天就來捋一捋。
我們先來看一張 WebSecurityConfigurerAdapter 的繼承關係圖:
在這層繼承關係中,有兩個非常重要的類:
-
SecurityBuilder
-
SecurityConfigurer
這兩個類松哥在之前的文章中都和大家分享過了,具體參考:
-
深入理解 HttpSecurity【源碼篇】 (本文講的是 SecurityBuilder 體系)
所以關於這兩個類的介紹以及作用,松哥這裏就不贅述了。咱們直接從 WebSecurityConfigurer 開始看起。
1.WebSecurityConfigurer
WebSecurityConfigurer 其實是一個空接口,但是它裏邊約束了一些泛型,如下:
public interface WebSecurityConfigurer<T extends SecurityBuilder<Filter>> extends SecurityConfigurer<Filter, T> { }
這裏邊的泛型很關鍵,這關乎到 WebSecurityConfigurer 的目的是啥!
-
SecurityBuilder 中的泛型 Filter,表示 SecurityBuilder 最終的目的是爲了構建一個 Filter 對象出來。
-
SecurityConfigurer 中兩個泛型,第一個表示的含義也是 SecurityBuilder 最終構建的對象。
同時這裏還定義了新的泛型 T,T 需要繼承自 SecurityBuilder
所以 WebSecurityConfigurer 的目的我們可以理解爲就是爲了配置 WebSecurity。
2.WebSecurity
我們來看下 WebSecurity 的定義:
public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter, WebSecurity> implements SecurityBuilder<Filter>, ApplicationContextAware { }
沒錯,確實是這樣!WebSecurity 繼承自 AbstractConfiguredSecurityBuilder<Filter, WebSecurity> 同時實現了 SecurityBuilder
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
SecurityBuilder 就是用來構建過濾器鏈的,在 HttpSecurity 實現 SecurityBuilder 時,傳入的泛型就是 DefaultSecurityFilterChain,所以 SecurityBuilder#build 方法的功能很明確,就是用來構建一個過濾器鏈出來,但是那個過濾器鏈是 Spring Security 中的。在 WebSecurityConfigurerAdapter 中定義的泛型是 SecurityBuilder
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【源碼篇】 。
把握住了這條主線,我們再來看方法的實現就很容易了。
-
首先統計過濾器鏈的總條數,總條數包括兩個方面,一個是 ignoredRequests,這是忽略的請求,通過 WebSecurity 配置的忽略請求,松哥之前介紹過,參見: Spring Security 兩種資源放行策略,千萬別用錯了! ,另一個則是 securityFilterChainBuilders,也就是我們通過 HttpSecurity 配置的過濾器鏈,有幾個就算幾個。
-
創建 securityFilterChains 集合,並且遍歷上面提到的兩種類型的過濾器鏈,並將過濾器鏈放入 securityFilterChains 集合中。
-
我在 深入理解 HttpSecurity【源碼篇】 一文中介紹過,HttpSecurity 構建出來的過濾器鏈對象就是 DefaultSecurityFilterChain,所以可以直接將 build 結果放入 securityFilterChains 中,而 ignoredRequests 中保存的則需要重構一下才可以存入 securityFilterChains。
-
securityFilterChains 中有數據之後,接下來創建一個 FilterChainProxy。
-
給新建的 FilterChainProxy 配置上防火牆,防火牆的介紹參考松哥之前的: Spring Security 自帶防火牆!你都不知道自己的系統有多安全! 。
-
最後我們返回的就是 FilterChainProxy 的實例。
從這段分析中,我們可以看出來 WebSecurity 和 HttpSecurity 的區別:
-
HttpSecurity 目的是構建過濾器鏈,一個 HttpSecurity 對象構建一條過濾器鏈,一個過濾器鏈中有 N 個過濾器,HttpSecurity 所做的事情實際上就是在配置這 N 個過濾器。
-
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,整體上來說並不難,但是要和松哥前面幾篇源碼分析文章一起看,理解會更加深刻一些。
傳送門:
好啦,小夥伴們要是有收穫,記得點個在看鼓勵下松哥哦~
今日干貨
剛剛發表
查看: 13500 回覆: 135
公衆號後臺回覆 SpringBoot,免費獲取 274 頁SpringBoot修煉手冊。