別辜負生命,別辜負自己。
楔子
前面兩期我講了SpringSecurity認證流程和SpringSecurity鑑權流程,今天是第三期,是SpringSecurity
的收尾工作,講SpringSecurity
的啟動流程。
就像很多電影拍火了之後其續作往往是前作的前期故事一樣,我這個第三期要講的SpringSecurity啟動流程
也是不擇不扣的"前期故事",它能幫助你真正認清SpringSecurity
的整體全貌。
在之前的文章裡,在說到SpringSecurity
中的過濾器鏈的時候,往往是把它作為一個概念瞭解的,就是我們只是知道有這麼個東西,也知道它到底是幹什麼用的,但是我們卻不知道這個過濾器鏈是由什麼類什麼時候去怎麼樣建立出來的。
今天這期就是要了解SpringSecurity
的自動配置到底幫我們做了什麼,它是如何把過濾器鏈給建立出來的,又是在預設配置的時候怎麼加入了我們的自定義配置。
祝有好收穫(邊贊邊看,法力無限)。
1. ?EnableWebSecurity
我們先來看看我們一般是如何使用SpringSecurity
的。
我們用SpringSecurity
的時候都會先新建一個SpringSecurity
相關的配置類,用它繼承WebSecurityConfigurerAdapter
,然後打上註解@EnableWebSecurity
,然後我們就可以通過重寫
WebSecurityConfigurerAdapter
裡面的方法來完成我們自己的自定義配置。
就像這樣:
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
}
}
我們已經知道,繼承WebSecurityConfigurerAdapter
是為了重寫配置,那這個註解是做了什麼呢?
從它的名字@EnableWebSecurity
我們可以大概猜出來,它就是那個幫我們自動配置了SpringSecurity
的好心人。
@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class,
SpringWebMvcImportSelector.class,
OAuth2ImportSelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {
/**
* Controls debugging support for Spring Security. Default is false.
* @return if true, enables debug support with Spring Security
*/
boolean debug() default false;
}
emmm,我猜大家應該有註解相關的知識吧,ok,既然你們都有註解相關的知識,我就直接講了。
這個@EnableWebSecurity
中有兩個地方是比較重要的:
一是
@Import
註解匯入了三個類,這三個類中的後兩個是SpringSecurity
為了相容性做的一些東西,相容SpringMVC
,相容SpringSecurityOAuth2
,我們主要看的其實是第一個類,匯入這個類代表了載入了這個類裡面的內容。二是
@EnableGlobalAuthentication
這個註解,@EnableWebSecurity
大家還沒搞明白呢,您這又來一個,這個註解呢,其作用也是載入了一個配置類-AuthenticationConfiguration
,看它的名字大家也可應該知道它載入的類是什麼相關的了吧,沒錯就是AuthenticationManager
相關的配置類,這個我們可以以後再說。
綜上所述,@EnableWebSecurity
可以說是幫我們自動載入了兩個配置類:WebSecurityConfiguration
和AuthenticationConfiguration
(@EnableGlobalAuthentication
註解載入了這個配置類)。
其中WebSecurityConfiguration
是幫助我們建立了過濾器鏈的配置類,而AuthenticationConfiguration
則是為我們注入AuthenticationManager
相關的配置類,我們今天主要講的是WebSecurityConfiguration
。
2. ?原始碼概覽
既然講的是WebSecurityConfiguration
,我們照例先把原始碼給大家看看,精簡了一下無關緊要的:
@Configuration(proxyBeanMethods = false)
public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {
private WebSecurity webSecurity;
private Boolean debugEnabled;
private List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers;
private ClassLoader beanClassLoader;
@Autowired(required = false)
private ObjectPostProcessor<Object> objectObjectPostProcessor;
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = webSecurityConfigurers != null
&& !webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
.postProcess(new WebSecurityConfigurerAdapter() {
});
webSecurity.apply(adapter);
}
return webSecurity.build();
}
@Autowired(required = false)
public void setFilterChainProxySecurityConfigurer(
ObjectPostProcessor<Object> objectPostProcessor,
@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)
throws Exception {
webSecurity = objectPostProcessor
.postProcess(new WebSecurity(objectPostProcessor));
if (debugEnabled != null) {
webSecurity.debug(debugEnabled);
}
webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE);
Integer previousOrder = null;
Object previousConfig = null;
for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {
Integer order = AnnotationAwareOrderComparator.lookupOrder(config);
if (previousOrder != null && previousOrder.equals(order)) {
throw new IllegalStateException(
"@Order on WebSecurityConfigurers must be unique. Order of "
+ order + " was already used on " + previousConfig + ", so it cannot be used on "
+ config + " too.");
}
previousOrder = order;
previousConfig = config;
}
for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
webSecurity.apply(webSecurityConfigurer);
}
this.webSecurityConfigurers = webSecurityConfigurers;
}
}
如程式碼所示,首先WebSecurityConfiguration
是個配置類,類上面打了@Configuration
註解,這個註解的作用大家還知道吧,在這裡就是把這個類中所有帶@Bean
註解的Bean給例項化一下。
這個類裡面比較重要的就兩個方法:springSecurityFilterChain
和setFilterChainProxySecurityConfigurer
。
springSecurityFilterChain
方法上打了@Bean
註解,任誰也能看出來就是這個方法建立了springSecurityFilterChain
,但是先彆著急,我們不能先看這個方法,雖然它在上面。
3. ?SetFilterChainProxySecurityConfigurer
我們要先看下面的這個方法:setFilterChainProxySecurityConfigurer
,為啥呢?
為啥呢?
因為它是@Autowired
註解,所以它要比springSecurityFilterChain
方法優先執行,從系統載入的順序來看,我們需要先看它。
@Autowired
在這裡的作用是為這個方法自動注入所需要的兩個引數,我們先來看看這兩個引數:
引數
objectPostProcessor
是為了建立WebSecurity
例項而注入進來的,先了解一下即可。引數
webSecurityConfigurers
是一個List,它實際上是所有WebSecurityConfigurerAdapter
的子類,那如果我們定義了自定義的配置類,其實就是把我們的配置也讀取到了。這裡其實有點難懂為什麼引數中
SecurityConfigurer<Filter, WebSecurity>
這個型別可以拿到WebSecurityConfigurerAdapter
的子類?因為
WebSecurityConfigurerAdapter
實現了WebSecurityConfigurer<WebSecurity>
介面,而WebSecurityConfigurer<WebSecurity>
又繼承了SecurityConfigurer<Filter, T>
,經過一層實現,一層繼承關係之後,WebSecurityConfigurerAdapter
終於成為了SecurityConfigurer
的子類。而引數中
SecurityConfigurer<Filter, WebSecurity>
中的兩個泛型引數其實是起到了一個過濾的作用,仔細檢視我們的WebSecurityConfigurerAdapter
的實現與繼承關係,你可以發現我們的WebSecurityConfigurerAdapter
正好是這種型別。
ok,說完了引數,我覺得我們可以看看程式碼了:
@Autowired(required = false)
public void setFilterChainProxySecurityConfigurer(
ObjectPostProcessor<Object> objectPostProcessor,
@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)
throws Exception {
// 建立一個webSecurity例項
webSecurity = objectPostProcessor
.postProcess(new WebSecurity(objectPostProcessor));
if (debugEnabled != null) {
webSecurity.debug(debugEnabled);
}
// 根據order排序
webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE);
Integer previousOrder = null;
Object previousConfig = null;
for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {
Integer order = AnnotationAwareOrderComparator.lookupOrder(config);
if (previousOrder != null && previousOrder.equals(order)) {
throw new IllegalStateException(
"@Order on WebSecurityConfigurers must be unique. Order of "
+ order + " was already used on " + previousConfig + ", so it cannot be used on "
+ config + " too.");
}
previousOrder = order;
previousConfig = config;
}
// 儲存配置
for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
webSecurity.apply(webSecurityConfigurer);
}
// 成員變數初始化
this.webSecurityConfigurers = webSecurityConfigurers;
}
根據我們的註釋,這段程式碼做的事情可以分為以為幾步:
建立了一個webSecurity例項,並且賦值給成員變數。
緊接著對
webSecurityConfigurers
通過order
進行排序,order
是載入順序。進行判斷是否有相同
order
的配置類,如果出現將會直接報錯。儲存配置,將其放入
webSecurity
的成員變數中。
大家可以將這些直接理解為成員變數的初始化,和載入我們的配置類配置即可,因為後面的操作都是圍繞它初始化的webSecurity
例項和我們載入的配置類資訊來做的。
這些東西還可以拆出來一步步的來講,但是這樣的話真是一篇文章寫不完,我也沒有那麼大的精力能夠事無鉅細的寫出來,我只挑選這條痕跡清晰的主脈絡來講,如果大家看完能明白它的一個載入順序其實就挺好了。
就像Spring的面試題會問SpringBean的載入順序,SpringMVC則會問SpringMVC一個請求的執行過程一樣。
全部弄得明明白白,必須要精研原始碼,在初期,我們只要知道它的一條主脈絡,在之後的使用中,哪出了問題你可以直接去定位到可能是哪有問題,這樣就已經很好了,學習是一個迴圈漸進的過程。
4. ?SpringSecurityFilterChain
初始化完變數,載入完配置,我們要開始建立過濾器鏈了,所以先走setFilterChainProxySecurityConfigurer
是有原因的,如果我們不把我們的自定義配置載入進來,建立過濾器鏈的時候怎麼知道哪些過濾器需要哪些過濾器不需要。
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = webSecurityConfigurers != null
&& !webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
.postProcess(new WebSecurityConfigurerAdapter() {
});
webSecurity.apply(adapter);
}
return webSecurity.build();
}
springSecurityFilterChain
方法邏輯就很簡單了,如果我們沒載入自定義的配置類,它就替我們載入一個預設的配置類,然後呼叫這個build
方法。
看到這熟悉的方法名稱,你就應該知道這是建造者模式,不管它什麼模式,既然呼叫了,我們點進去就是了。
public final O build() throws Exception {
if (this.building.compareAndSet(false, true)) {
this.object = doBuild();
return this.object;
}
throw new AlreadyBuiltException("This object has already been built");
}
build()
方法是webSecurity
的父類AbstractSecurityBuilder
中的方法,這個方法又呼叫了doBuild()
方法。
@Override
protected final O doBuild() throws Exception {
synchronized (configurers) {
buildState = AbstractConfiguredSecurityBuilder.BuildState.INITIALIZING;
// 空方法
beforeInit();
// 呼叫init方法
init();
buildState = AbstractConfiguredSecurityBuilder.BuildState.CONFIGURING;
// 空方法
beforeConfigure();
// 呼叫configure方法
configure();
buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILDING;
// 呼叫performBuild
O result = performBuild();
buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILT;
return result;
}
}
通過我的註釋可以看到beforeInit()
和beforeConfigure()
都是空方法,
實際有用的只有init()
,configure()
和performBuild()
方法。
我們先來看看init()
,configure()
方法。
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);
}
}
原始碼中可以看到都是先獲取到我們的配置類資訊,然後迴圈呼叫配置類自己的init()
,configure()
方法。
前面說過,我們的配置類是繼承了WebSecurityConfigurerAdapter
的子類,而WebSecurityConfigurerAdapter
又是SecurityConfigurer
的子類,所有SecurityConfigurer
的子類都需要實現init()
,configure()
方法。
所以這裡的init()
,configure()
方法其實就是呼叫WebSecurityConfigurerAdapter
自己重寫的init()
,configure()
方法。
其中WebSecurityConfigurerAdapter
中的configure()
方法是一個空方法,所以我們只需要去看WebSecurityConfigurerAdapter
中的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);
});
}
這裡也可以分為兩步:
執行了
getHttp()
方法,這裡面初始化加入了很多過濾器。將
HttpSecurity
放入WebSecurity
,將FilterSecurityInterceptor
放入WebSecurity
,就是我們鑑權那章講過的FilterSecurityInterceptor
。
那我們主要看第一步getHttp()
方法:
protected final HttpSecurity getHttp() throws Exception {
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;
}
getHttp()
方法裡面http
呼叫的那一堆方法都是一個個過濾器,第一個csrf()
很明顯就是防止CSRF攻擊的過濾器,下面還有很多,這就是SpringSecurity
預設會加入過濾器鏈的那些過濾器了。
其次,還有一個重點就是倒數第二行程式碼,我也加上了註釋,我們一般在我們自定義的配置類中重寫的就是這個方法,所以我們的自定義配置就是在這裡生效的。
所以在初始化的過程中,這個方法會先載入自己預設的配置然後再載入我們重寫的配置,這樣兩者結合起來,就變成了我們看到的預設配置。(如果我們不重寫configure(http)
方法,它也會一點點的預設配置,大家可以去看原始碼,看了就明白了。)
init()
,configure()
(空方法)結束之後,就是呼叫performBuild()
方法。
protected Filter performBuild() throws Exception {
int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();
List<SecurityFilterChain> securityFilterChains = new ArrayList<>(
chainSize);
for (RequestMatcher ignoredRequest : ignoredRequests) {
securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
}
// 呼叫securityFilterChainBuilder的build()方法
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;
postBuildAction.run();
return result;
}
這個方法主要需要看的是呼叫securityFilterChainBuilder
的build()
方法,這個securityFilterChainBuilder
是我們在init()
方法中add的那個,所以這裡的securityFilterChainBuilder
其實就是HttpSecurity
,所以這裡其實是呼叫了HttpSecurity
的bulid()
方法。
又來了,WebSecurity
的bulid()
方法還沒說完,先來了一下HttpSecurity
的bulid()
方法。
HttpSecurity
的bulid()
方法程式和之前的一樣,也是先init()
然後configure()
最後performBuild()
方法,值得一提的是在HttpSecurity
的performBuild()
方法裡面,會對過濾器鏈中的過濾器進行排序:
@Override
protected DefaultSecurityFilterChain performBuild() {
filters.sort(comparator);
return new DefaultSecurityFilterChain(requestMatcher, filters);
}
HttpSecurity
的bulid()
方法執行完了之後將DefaultSecurityFilterChain
返回給WebSecurity
的performBuil()
方法,performBuil()
方法再將其轉換為FilterChainProxy
,最後WebSecurity
的performBuil()
方法執行結束,返回一個Filter
注入成為name="springSecurityFilterChain"
的Bean
。
經過以上這些步驟之後,springSecurityFilterChain
方法執行完畢,我們的過濾器鏈就建立完成了,SpringSecurity
也可以跑起來了。
後記
看到這的話,其實你已經很有耐性了,但可能還覺得雲裡霧裡的,因為SpringSecurity
(Spring大家族)這種工程化極高的專案專案都是各種設計模式和編碼思想滿天飛,看不懂的時候只能說這什麼玩意,看得懂的時候又該膜拜這是藝術啊。
這些東西它不容易看懂但是比較解耦容易擴充套件,像一條線下來的程式碼就容易看懂但是不容易擴充套件了,福禍相依。
而且這麼多名稱相近的類名,各種繼承抽象,要好好理解下來的確沒那麼容易,這篇其實想給這個SpringSecurity
來個收尾,逼著自己寫的,我這個人喜歡有始有終,這段東西也的確複雜,接下來的幾篇打算寫幾個實用的有意思的也輕鬆的放鬆一下。
如果你對SpringSecurity
原始碼有興趣可以跟著來我這個文章,點開你自己的原始碼點一點,看一看,加油。
自從上篇徵文發了之後,感覺多了很多前端的關注者,掘金果然還是前端多啊,沒事,雖然我不怎麼寫前端,說不定哪天改行了呢哈哈。
我也不藏著掖著,其實我現在是寫後端的,我對前端呢只能說是略懂略懂,不過無聊了也可以來看看我的文章,點點贊刷刷閱讀乾乾啥的?,說不定某一天突然看懂了某篇文還前端勸退後端入行,加油了大家。
別辜負生命,別辜負自己。
你們的每個點贊收藏與評論都是對我知識輸出的莫大肯定,如果有文中有什麼錯誤或者疑點或者對我的指教都可以在評論區下方留言,一起討論。