Spring Security 實戰乾貨:AuthenticationManager的初始化細節

1. 前言

今天有個同窗告訴我,在Security Learning項目的day11分支中出現了一個問題,驗證碼登陸和其它登陸不兼容了,出現了No Provider異常。還有這事?我趕忙跑了一遍還真是,看來我大意了,不過最終找到了緣由,問題就出在AuthenticationManager的初始化上。自定義了一個UseDetailServiceAuthenticationProvider以後AuthenticationManager的默認初始化出問題了。html

雖然在Spring Security 實戰乾貨:圖解認證管理器AuthenticationManager一文中對AuthenticationManager的流程進行了分析,可是仍是不夠深刻,以致於出現了問題。今天就把這個坑補了。java

2. AuthenticationManager的初始化

關於AuthenticationManager的初始化,流程部分請看這一篇文章,裏面有流程圖。在流程圖中咱們提到了AuthenticationManager的默認初始化是由AuthenticationConfiguration完成的,可是隻是一筆帶過,具體的細節沒有搞清楚。如今就搞定它。app

AuthenticationConfiguration

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

第一個問題是AuthenticationManagerBuilder是如何注入Spring IoC的?ui

AuthenticationManagerBuilder注入的過程也是在AuthenticationConfiguration中完成的,注入的是其內部的一個靜態類DefaultPasswordEncoderAuthenticationManagerBuilder,這個類和Spring Security的主配置類WebSecurityConfigurerAdapter的一個內部類同名,這兩個類幾乎邏輯相同,沒有什麼特別的。具體使用哪一個由WebSecurityConfigurerAdapter.disableLocalConfigureAuthenticationBldr決定。this

其參數ObjectPostProcessor<T>抽空會講它的做用。代理

GlobalAuthenticationConfigurerAdapter

另外一個問題是GlobalAuthenticationConfigurerAdapter從哪兒來?code

AuthenticationConfiguration包含下面自動注入GlobalAuthenticationConfigurerAdapter的方法:htm

@Autowired(required = false)
public void setGlobalAuthenticationConfigurers(
      List<GlobalAuthenticationConfigurerAdapter> configurers) {
   configurers.sort(AnnotationAwareOrderComparator.INSTANCE);
   this.globalAuthConfigurers = configurers;
}

該方法會根據它們各自的Order進行排序。該排序的意義在於AuthenticationManagerBuilder在執行構建AuthenticationManager時會按照排序的前後執行GlobalAuthenticationConfigurerAdapterconfigure方法。

全局認證配置

第一個爲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流程差很少,只不過這裏主要處理的是UserDetailsServiceDaoAuthenticationProvider。當執行到上面這個方法時,若是 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

相關文章
相關標籤/搜索