今天有個同窗告訴我,在Security Learning項目的day11分支中出現了一個問題,驗證碼登陸和其它登陸不兼容了,出現了No Provider異常。還有這事?我趕忙跑了一遍還真是,看來我大意了,不過最終找到了緣由,問題就出在AuthenticationManager
的初始化上。自定義了一個UseDetailService
和AuthenticationProvider
以後AuthenticationManager
的默認初始化出問題了。html
雖然在Spring Security 實戰乾貨:圖解認證管理器AuthenticationManager一文中對AuthenticationManager
的流程進行了分析,可是仍是不夠深刻,以致於出現了問題。今天就把這個坑補了。java
關於AuthenticationManager
的初始化,流程部分請看這一篇文章,裏面有流程圖。在流程圖中咱們提到了AuthenticationManager
的默認初始化是由AuthenticationConfiguration
完成的,可是隻是一筆帶過,具體的細節沒有搞清楚。如今就搞定它。app
AuthenticationConfiguration
初始化AuthenticationManager
的核心方法就是下面這個方法:ide
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
是如何注入Spring IoC的?ui
AuthenticationManagerBuilder
注入的過程也是在AuthenticationConfiguration
中完成的,注入的是其內部的一個靜態類DefaultPasswordEncoderAuthenticationManagerBuilder
,這個類和Spring Security的主配置類WebSecurityConfigurerAdapter
的一個內部類同名,這兩個類幾乎邏輯相同,沒有什麼特別的。具體使用哪一個由WebSecurityConfigurerAdapter.disableLocalConfigureAuthenticationBldr
決定。this
其參數
ObjectPostProcessor<T>
抽空會講它的做用。代理
另外一個問題是
GlobalAuthenticationConfigurerAdapter
從哪兒來?code
AuthenticationConfiguration
包含下面自動注入GlobalAuthenticationConfigurerAdapter
的方法:htm
@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
的注入。
到此爲何在認證的時候找不到緣由終於找到了,原來我在使用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
。
這一篇對於須要多種認證方式並存的Spring Security配置很是重要,若是你在配置中不注意,很容易引起No Provider ……
的異常。因此有頗有必要學習一下。
關注公衆號:Felordcn 獲取更多資訊