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

碼農小胖哥發表於2021-01-25

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

相關文章