1. 前言
今天有個同學告訴我,在Security Learning專案的day11分支中出現了一個問題,驗證碼登入和其它登入不相容了,出現了No Provider異常。還有這事?我趕緊跑了一遍還真是,看來我大意了,不過最終找到了原因,問題就出在AuthenticationManager
的初始化上。自定義了一個UseDetailService
和AuthenticationProvider
之後AuthenticationManager
的預設初始化出問題了。
雖然在Spring Security 實戰乾貨:圖解認證管理器AuthenticationManager一文中對AuthenticationManager
的流程進行了分析,但是還是不夠深入,以至於出現了問題。今天就把這個坑補了。
2. AuthenticationManager的初始化
關於AuthenticationManager
的初始化,流程部分請看這一篇文章,裡面有流程圖。在流程圖中我們提到了AuthenticationManager
的預設初始化是由AuthenticationConfiguration
完成的,但是隻是一筆帶過,具體的細節沒有搞清楚。現在就搞定它。
AuthenticationConfiguration
AuthenticationConfiguration
初始化AuthenticationManager
的核心方法就是下面這個方法:
public AuthenticationManager getAuthenticationManager() throws Exception {
// 先判斷 AuthenticationManager 是否初始化
if (this.authenticationManagerInitialized) {
// 如果已經初始化 那麼直接返回初始化的
return this.authenticationManager;
}
// 否則就去 Spring IoC 中獲取其構建類
AuthenticationManagerBuilder authBuilder = this.applicationContext.getBean(AuthenticationManagerBuilder.class);
// 如果不是第一次構建 好像是每次總要通過Builder來進行構建
if (this.buildingAuthenticationManager.getAndSet(true)) {
// 返回 一個委託的AuthenticationManager
return new AuthenticationManagerDelegator(authBuilder);
}
// 如果是第一次通過Builder構建 將全域性的認證配置整合到Builder中 那麼以後就不用再整合全域性的配置了
for (GlobalAuthenticationConfigurerAdapter config : globalAuthConfigurers) {
authBuilder.apply(config);
}
// 構建AuthenticationManager
authenticationManager = authBuilder.build();
// 如果構建結果為null
if (authenticationManager == null) {
// 再次嘗試去Spring IoC 獲取懶載入的 AuthenticationManager Bean
authenticationManager = getAuthenticationManagerBean();
}
// 修改初始化狀態
this.authenticationManagerInitialized = true;
return authenticationManager;
}
根據上面的註釋,AuthenticationManager
的初始化流程是清楚的。但是又引出來了兩個問題,我將另起兩個章節來分析這兩個問題。
AuthenticationManagerBuilder
第一個問題是
AuthenticationManagerBuilder
是如何注入Spring IoC的?
AuthenticationManagerBuilder
注入的過程也是在AuthenticationConfiguration
中完成的,注入的是其內部的一個靜態類DefaultPasswordEncoderAuthenticationManagerBuilder
,這個類和Spring Security的主配置類WebSecurityConfigurerAdapter
的一個內部類同名,這兩個類幾乎邏輯相同,沒有什麼特別的。具體使用哪個由WebSecurityConfigurerAdapter.disableLocalConfigureAuthenticationBldr
決定。
其引數
ObjectPostProcessor<T>
抽空會講它的作用。
GlobalAuthenticationConfigurerAdapter
另一個問題是
GlobalAuthenticationConfigurerAdapter
從哪兒來?
AuthenticationConfiguration
包含下面自動注入GlobalAuthenticationConfigurerAdapter
的方法:
@Autowired(required = false)
public void setGlobalAuthenticationConfigurers(
List<GlobalAuthenticationConfigurerAdapter> configurers) {
configurers.sort(AnnotationAwareOrderComparator.INSTANCE);
this.globalAuthConfigurers = configurers;
}
該方法會根據它們各自的Order
進行排序。該排序的意義在於AuthenticationManagerBuilder
在執行構建AuthenticationManager
時會按照排序的先後執行GlobalAuthenticationConfigurerAdapter
的configure
方法。
全域性認證配置
第一個為EnableGlobalAuthenticationAutowiredConfigurer
,它目前除了列印一下初始化資訊沒有什麼實際作用。
認證處理器初始化注入
第二個為InitializeAuthenticationProviderBeanManagerConfigurer
,核心方法為其內部類的實現:
@Override
public void configure(AuthenticationManagerBuilder auth) {
//
// 如果存在 AuthenticationProvider 已經注入 或者 已經有AuthenticationManager被代理
if (auth.isConfigured()) {
return;
}
// 嘗試從Spring IoC獲取 AuthenticationProvider
AuthenticationProvider authenticationProvider = getBeanOrNull(
AuthenticationProvider.class);
// 獲取不到就中斷
if (authenticationProvider == null) {
return;
}
// 獲取得到就配置到AuthenticationManagerBuilder中,最終會配置到AuthenticationManager中
auth.authenticationProvider(authenticationProvider);
}
這裡的getBeanOrNull
方法如果不仔細看的話是有誤區的,核心程式碼如下:
String[] userDetailsBeanNames = InitializeUserDetailsBeanManagerConfigurer.this.context
.getBeanNamesForType(type);
// Spring IoC 不能同時存在多個type相關型別的Bean 否則無法注入
if (userDetailsBeanNames.length != 1) {
return null;
}
如果 Spring IoC 容器中存在了多個AuthenticationProvider
,那麼這些AuthenticationProvider
就不會生效。
使用者詳情管理器初始化注入
第三個為InitializeUserDetailsBeanManagerConfigurer
,優先順序低於上面。它的核心方法為:
public void configure(AuthenticationManagerBuilder auth) throws Exception {
if (auth.isConfigured()) {
return;
}
// 不能有多個 否則 就中斷
UserDetailsService userDetailsService = getBeanOrNull(
UserDetailsService.class);
if (userDetailsService == null) {
return;
}
// 開始配置普通 密碼認證器 DaoAuthenticationProvider
PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
if (passwordEncoder != null) {
provider.setPasswordEncoder(passwordEncoder);
}
if (passwordManager != null) {
provider.setUserDetailsPasswordService(passwordManager);
}
provider.afterPropertiesSet();
auth.authenticationProvider(provider);
}
跟InitializeAuthenticationProviderBeanManagerConfigurer
流程差不多,只不過這裡主要處理的是UserDetailsService
、DaoAuthenticationProvider
。當執行到上面這個方法時,如果 Spring IoC 容器中存在了多個UserDetailsService
,那麼這些UserDetailsService
就不會生效,影響DaoAuthenticationProvider
的注入。
3. 真相大白
到此為什麼在認證的時候找不到原因終於找到了,原來我在使用Spring Security預設配置時(注意這個前提),向Spring IoC注入了多個UserDetailsService
導致DaoAuthenticationProvider
沒有生效。也就是說在一套配置中如果你存在多個UserDetailsService
的Spring Bean將會影響DaoAuthenticationProvider
的注入。
但是我仍然需要注入多個
AuthenticationProvider
怎麼辦?
首先把你需要配置的AuthenticationProvider
注入Spring IoC,然後在HttpSecurity
中這麼寫:
protected void configure(HttpSecurity http) throws Exception {
ApplicationContext context = http.getSharedObject(ApplicationContext.class);
CaptchaAuthenticationProvider captchaAuthenticationProvider = context.getBean("captchaAuthenticationProvider", CaptchaAuthenticationProvider.class);
http.authenticationProvider(captchaAuthenticationProvider);
// 省略
}
有幾個AuthenticationProvider
你就按照上面配置幾個。
一般情況下一個
UserDetailsService
對應一個AuthenticationProvider
。
4. 總結
這一篇對於需要多種認證方式並存的Spring Security配置非常重要,如果你在配置中不注意,很容易引發No Provider ……
的異常。所以有很有必要學習一下。
關注公眾號:Felordcn 獲取更多資訊