Spring Security小教程 Vol 5.核心元件AuthenticationManager專題
我們在某一期其實已經對 Authentication
身份驗證中的主要元件進行過介紹,並且通過幾期的分享讓大家大致瞭解了Web應用大致利用核心進行身份驗證的流程和相關的擴充套件點。 這一期我們用一期的篇章把關注點放在 Authentication
身份驗證核心最主要的幾個服務 AuthenticationMananger
、 AuthenticationProvider
和 UserDetailsService
,三個頂層介面進行展開。通過Spring Security對這些介面服務的實現進行說明講解。目的是為了將來客製化擴充套件核心服務做好知識儲備。
第五期 核心元件AuthenticationManager專題
本期的任務清單
- AuthenticationMananger與ProviderMananger
- Authentication與AuthenticationProvider
- UserDetailsService介面和它的實現類
零、整體概述
本期的重點是Authentication身份驗證幾個核心服務介面:
- AuthenticationMananger
- AuthenticationProvide
- UserDetailsServic
額外的還包括兩個負責封裝使用者身份資訊的介面與類:
- Authentication
- UserDetails
我們回顧下前幾期我們在分享Spring Security核心元件時候曾用到過以下這張圖比較重要的核心元件:

這一期的重點顯而易見便是紅框中身份驗證部分的三個核心元件以及其相關的元件。
一、AuthenticationMananger與ProviderMananger
AuthenticationMananger
作為整個身份驗證核心最外層的封裝負責與外部使用者進行互動。 AuthenticationMananger
介面有且僅有一個對外的服務便是“身份驗證”。這樣是整個身份驗證服務對外提供的服務介面。
Authentication authenticate(Authentication authentication) throws AuthenticationException; 複製程式碼
外部使用者通過將身份驗證的必要資訊,比如使用者名稱和密碼封裝一個Authentication傳遞、呼叫AuthenticationMananger的authenticate方法。如果沒有返回異常和null值,那麼驗證服務便是完成。完成身份驗證的Authentication不僅包含了使用者的身份驗證資訊,比如使用者名稱,額外還會將該使用者身份下所有對應的許可權列表也一併封裝返回。

身份資訊互動的紐帶:Authentication
在整個與外部使用互動的過程中 Authentication
的職責有兩個,第一個是封裝了驗證請求的引數,第二個便是封裝了使用者的許可權資訊。結合 Authentication
的介面設計便更加清晰了這樣的設計意圖:principal用於存放使用者的身份標識資訊,比如使用者名稱,credentials用於存放使用者的驗證憑證比如密碼,authorities用於存放使用者的許可權列表。而details則存放了除了使用者名稱和密碼其他可能會被用於身份驗證的資訊,比如應用限定使用者的使用ip範圍場景下,ip資訊可能便會被存放在details做輔助的驗證資訊使用。

唯一的實現類:ProviderMananger
為了向外部提供身份驗證服務,Spring Security中通過 ProviderMananger
實現了 AuthenticationManager
的身份驗證介面。作為實現類 ProviderMananger
便不能和 AuthenticationManager
一樣只關心唯一的抽象核心服務authenticate。在 ProviderMananger
為了管理外部輸入與像外部返回 的Authentication
, ProviderMananger
內部大致的工序如下:
- 首先,尋找可以進行驗證當前外部輸入
Authentication
形式的AuthenticationProvider
;如果自身的providers中無法處理驗證並且當前層次的Mananger
還有父級的Mananger
則向上傳遞,交由父層Mananger
進行處理; - 然後,因為details的資訊是外部傳入的,內部身份驗證後的
Authentication
並不會從持久化或者其他資料來源中攜帶,在返回前將details寫入返回給外部的Authentication
; - 最後,如果有必要則將外部身份驗證請求中的敏感擦除,比如講請求驗證的密碼置空。 瞭解了
ProviderMananger
完成的三件工作,大致明白了雖然整個驗證框架只有一個ProviderManager
暴露在外部,但是其內部可能是有多個AuthenticationMananger
和AuthenticationProvider
組成的網路,並且最終進行核心身份驗證的還是AuthenticationProvider
。核心在葉子節點中依次尋找對驗證當前Authentication
形式的AuthenticationProvider
。如果存在支援便將驗證請求的Authentication
傳遞給AuthenticationProvider
,委託其進行驗證。在處理輸入的驗證請求Authentication
,ProviderMananger
並不對其進行任何的處理,而是指在處理完後進行必要的加工和處理。
二、 Authentication與AuthenticationProvider
相對AuthenticationMananger而言AuthenticationProvider的工作更加明確:針對特定的驗證資料,提供特定的驗證行為。在這個語境下,Authentication的設計目的是解決驗證什麼(What)的問題,而authenticate方法更像是在回答怎麼驗證的問題(How)。
AuthenticationProvider視角中的Authentication
那麼我們先對驗證資料也就是Authentication的設計進行展開討論。Authentication主要職責就是封裝身份驗證時候需要的資訊資料,比如使用者名稱場景下的使用者名稱和密碼,簡訊驗證碼下的手機號碼和驗證碼,OAuth2場景下的ID和Code。總之每個不同驗證協議使用的驗證資訊都需要被被封裝成Authentication,更準確說在Spring Security把這種封裝了使用者身份驗證資訊的Authentication具體為了 的概念,畢竟一說token更容易理解。所有Spring Security中提供的各種協議的身份驗證資料的封裝都繼承 ,基於使用者名稱和密碼的UsernamePasswordAuthenticationToken,基於OAuth2的OAuth2AuthorizationCodeAuthenticationToken,基於CAS的CasAssertionAuthenticationToken。 通常我們使用使用者名稱和密碼的場景是最多,無論是使用基於資料庫持久化的使用者名稱密碼方案還是基於LDAP的使用者名稱和密碼方法。雖然驗證在驗證協實現有細微差別,但是無論使用驗證LDAP還是資料庫進行身份驗證比對,因為使用者提交的驗證身份資訊幾乎一致,我們便可以複用通用結構的AuthenticationToken——將username賦值到principal屬性並將password賦值到cencredentials屬性中。這樣就意味著我們在AuthenticationToken設計上最需要考慮是資料的封裝,而不是身份驗證行為的實現。

我們已經解決了第一個問題,在AuthenticationProvider驗證資料放都可以通過傳入Authentication的各種實現類AuthenticationToken進行獲取。下一個問題便是AuthenticationProvider是如何進行身份驗證的。 我們假設的場景是需要對使用使用者名稱和密碼的UsernamePasswordAuthenticationToken進行驗證。 在Spring Security針對UsernamePasswordAuthenticationToken進行身份驗證的有主要有兩個AuthenticationProvider:一個是基於Dao模型與資料層使用者資訊對比驗證的DaoAuthenticationProvider,另外一個是雖然同樣使用使用者名稱和密碼,但是驗證流程更加複雜,且使用者資料是通過與LDAP服務進行使用者驗證的LdapAuthenticationProvider。在這裡使用最廣泛使用的基於Dao的DaoAuthenticationProvider進行說明,如果有對Ldap實現有興趣的相信在看完對DaoAuthenticationProvider的分析之後再閱讀LdapAuthenticationProvider部分的程式碼就會輕鬆許多。

DaoAuthenticationProvider
DaoAuthenticationProvider為了實現外部的驗證請求便需要對外部傳遞身份資訊——使用者名稱和密碼進行驗證。我們把這個任務進一步分解成兩個獨立的任務:
- 從資料層獲取對應使用者名稱在資料層的資料記錄;
- 對外部的使用者名稱、密碼與資料層的使用者名稱、密碼進行比對。
為什麼在DaoAuthenticationProvider會將驗證任務再分解成這兩個獨立任務,最大原因便是,這兩個任務一個感知外部資源,另一個感知驗證演算法,兩種都是不同使用者可能存在不同的使用場景,框架並無法控制具體的實現。 第一個任務,從資料層獲取對應使用者名稱在資料層的資料記錄,我們的目標是從資料層中查詢到我們需要比對的使用者身份資料,但是在這個場景下我們無非控制的是資料層的實現具體是什麼?是通過JDBC訪問Mysql還是通過JPA訪問Oracle,更或是直接通過記憶體訪問一個儲存了使用者資訊鍵值對的Map? 第二個任務,對外部的使用者名稱、密碼與資料層的使用者名稱、密碼進行比對,具體的加密演算法是什麼?如何實現的? 這兩個問題在DaoAuthenticationProvider中都無法給出明確的實現。Spring Security便將這兩種在DaoAuthenticationProvider無法確定、存在變化的行為分別委託給了DaoAuthenticationProvider兩個重要元件去完成:
- 通過使用者名稱返回資料層中的使用者資訊的UserDetailsService;
- 通過特定加密演算法處理使用者密碼的PasswordEncoder。
使用UserDetailsService獲取內部使用者身份資訊
UserDetailsService介面定位從他的介面方法就可以明白,就是向核心元件們提供資料層的使用者資訊,而使用者資訊在這裡被封裝成了UserDetails。

UserDetailsService中只有一個方法便是loadUserByUsername方法,通過傳入使用者名稱返回資料層的使用者身份記錄。
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; 複製程式碼
當我們確定我們獲取使用者身份資訊的方法之後,我們便可以自行擴充套件UserDetailsService方法,告知框架如何獲取使用者身份資訊。通常這個步驟是使用Spring Security中是必須完成的工作。 我們在第一個章節中曾經寫過以下程式碼用於配置我們使用的UserDetailsService:
public class WebSecurityConfigextends WebSecurityConfigurerAdapter { //注入新的UserDetailsServiceBean @Bean public UserDetailsService userDetailsService() { InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); return manager; } } 複製程式碼
那一次我們使用了基於記憶體鍵值對的形式來儲存和獲取使用者資訊記錄。同樣的我們也可以通過JDBC和JPA來獲取使用者身份資訊,而Spring Security很貼心的已經提供了一個基於JDBC的UserServiceDetails實現和對應的模板DDL:

create table users(username varchar_ignorecase(50) not null primary key,password varchar_ignorecase(500) not null,enabled boolean not null); create table authorities (username varchar_ignorecase(50) not null,authority varchar_ignorecase(50) not null,constraint fk_authorities_users foreign key(username) references users(username)); create unique index ix_auth_username on authorities (username,authority); 複製程式碼
而通過UserDetailsService返回的資料型別是UserDetails,UserDetails中封裝了使用者名稱、密碼和授權資訊同時還額外包括了一些過期和鎖定的標識屬性。我們不難發現UserDetails封裝的資料和Authentication非常的相似。沒錯,在身份驗證成功後,DaoAuthenticationProvider便會將內部的UserDetails抽離必要的資料對應賦值到UsernamePasswordAuthenticationToken最終返回給外部呼叫者進行使用。我們可以簡單的把UserDetails理解為使用者身份資訊在資料層的封裝。在客製化的過程中,如果使用者資訊的資料結構是比較特殊的結構,比如Ldap,那麼便可以自行擴充套件UserDetails客製化一個特殊的結構用於獲取使用者資料記錄。 說到這裡基本上我們已經瞭解了DaoAuthenticationProvider兩個元件中用於獲取使用者身份記錄的UserDetailsService部分,下面我們介紹下處理密碼驗證的PasswordEncoder部分。
使用PasswordEncoder來進行密碼的比對
我們繼續追蹤上面的場景來說下關於驗證演算法部分。DaoAuthenticationProvider收到了外部提交的使用者名稱和密碼,同樣的DaoAuthenticationProvider也查詢到了對應使用者名稱在資料庫中的使用者名稱和密碼。通常情況下雖然都是密碼,資料庫中儲存的密碼通常會進行過一定的加密。DaoAuthenticationProvider便需要將外部提交的使用者名稱和密碼進行一次加密流程並進行比對。舉個例子我們當前使用的演算法比如是MD5,加密明文的樣本是使用者的使用者名稱拼接使用者名稱。如有當前需要身份驗證的請求中使用者名稱是admin,密碼是password。同樣的在資料庫中加密後的密碼是9b02edfbc208a538。我們便需要對外部傳遞的使用者名稱和密碼做一個MD5("passwordadmin")得到9b02edfbc208a538,再與資料庫中的password欄位進行對比,如果一致則認定驗證成功。 在Spring Security中這種針對處理稱為 ,PasswordEncoder介面主要的作用就是對明文密碼進行加密與比對。


Spring Security中預設向DaoAuthenticationProvider提供的PasswordEncoder是BCryptPasswordEncoder。如果對BCrypt可以額外通過谷歌去了解加密流程。 如果我們需要客製化自己的加密演算法,只要實現PasswordEncoder介面,並重新通過Spring注入DaoAuthenticationProvider便可以了。
@Bean public PasswordEncoder passwordEncoder() { //通過修改注入的例項,客製化自己的PasswordEncoder return new BCryptPasswordEncoder(); } 複製程式碼
PasswordEncoder 部分的功能相對較少,通常情況下使用預設提供的BCryptPasswordEncoder就足夠完成任務。
結尾
在本期我們花了很大篇幅介紹了身份驗證核心中最主要個幾個元件和基於一個使用使用者名稱和密碼驗證場景下對應介面的實現類的具體職責:
- AuthenticationManager負責核心驗證前後的處理,並且負責與外部呼叫者進行互動;
- AuthenticationProvider是驗證服務的核心實現,其驗證的資料形式是AuthenticationToken其中封裝了驗證使用的使用者標識和使用者驗證憑證資訊;
- AuthenticationProvider中獲取內部使用者身份資訊是通過UserDetailsService完成的。如需要進行密碼處理,則引入了PasswordEncoder;
- UserDetails封裝了使用者資訊在內部的結構,在向外部返回Authentication之前,AuthenticationProvider通常會將UserDetails必要的資料複製到向外部返回的AuthenticationToken中。
通過幾期的說明,整個Spring Security關於身份驗證的元件、流程和特定場景的實現基本我們都瞭解了一遍。從下一期開始,我們會開始講解訪問控制部分、框架配置部分的設計與概念。同時也將不定期通過一些場景的實戰強相關框架概念和設計理念。 謝謝大家,我們下期再見。