
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 獲取更多資訊
個人博客:https://felord.cn
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/252435.html
標籤:Java
上一篇:JVM-類加載
下一篇:Java排序演算法(二)選擇排序
