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


1. 前言

今天有個同學告訴我,在Security Learning項目的day11分支中出現了一個問題,驗證碼登錄和其它登錄不兼容了,出現了No Provider異常。還有這事?我趕緊跑了一遍還真是,看來我大意了,不過最終找到了原因,問題就出在AuthenticationManager的初始化上。自定義了一個UseDetailServiceAuthenticationProvider之后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時會按照排序的先后執行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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM