前言
上一期我們分享了Spring Security是如何通過AbstractAuthenticationProcessingFilter向Web應(yīng)用向基于HTTP、瀏覽器的請求提供身份驗(yàn)證服務(wù)的。
這一次我們針對最常用,也是Spring Security默認(rèn)在HTTP上使用的驗(yàn)證過濾器即基于用戶名和密碼的身份驗(yàn)證過濾器是如何與核心進(jìn)行交互進(jìn)行展開說明。目的是希望讓大家對如何在Spring Security的核心上完成一個(gè)指定的身份驗(yàn)證協(xié)議的擴(kuò)展工作,已經(jīng)涉及相關(guān)主要組件及其角色職責(zé)有個(gè)初步的了解。
這一期的內(nèi)容如果有了前幾期對身份驗(yàn)證核心的背景,相對來說比較的簡單,因?yàn)檎麄€(gè)流程就是在原有的基礎(chǔ)上更加具體化了場景:身份驗(yàn)證的數(shù)據(jù)來源是用戶提交的請求,驗(yàn)證的憑證是用戶名和密碼。由于這樣的原因,這一期更像是對前幾期的一個(gè)綜合性的應(yīng)用總結(jié)。
第四期 UsernamePasswordAuthenticationFilter詳細(xì)說明
本期的任務(wù)清單
- 了解UsernamePasswordAuthenticationFilter的職責(zé)和實(shí)現(xiàn)
1. 了解UsernamePasswordAuthenticationFilter的職責(zé)和實(shí)現(xiàn)
UsernamePasswordAuthenticationFilter類的說明
UsernamePasswordAuthenticationFilter是AbstractAuthenticationProcessingFilter針對使用用戶名和密碼進(jìn)行身份驗(yàn)證而定制化的一個(gè)過濾器。
在一開始我們先通過下面的配圖來回憶一下我們的老朋友AbstractAuthenticationProcessingFilter的在框架中的角色與職責(zé)。

AbstractAuthenticationProcessingFilter在整個(gè)身份驗(yàn)證的流程中主要處理的工作就是所有與Web資源相關(guān)的事情,并且將其封裝成Authentication對象,最后調(diào)用AuthenticationManager的驗(yàn)證方法。所以UsernamePasswordAuthenticationFilter的工作大致也是如此,只不過在這個(gè)場景下更加明確了Authentication對象的封裝數(shù)據(jù)的來源和形式——使用用戶名和密碼。
接著我們再對的屬性和方法做一個(gè)快速的了解。UsernamePasswordAuthenticationFilter繼承擴(kuò)展了AbstractAuthenticationProcessingFilter,相對與AbstractAuthenticationProcessingFilter而言主要有以下幾個(gè)改動(dòng):
- 屬性中增加了username和password字段;
- 強(qiáng)制的只對POST請求應(yīng)用;
- 重寫了attemptAuthentication身份驗(yàn)證入口方法。

封裝用戶名密碼的基石:UsernamePasswordAuthenticationToken
在UsernamePasswordAuthenticationFilter的屬性聲明中額外增加了username和password的動(dòng)機(jī)很容易明白,即需要從HttpRequest中獲取對應(yīng)的參數(shù)字段,并將其封裝進(jìn)Authentication中傳遞給AuthenticationManager進(jìn)行身份驗(yàn)證。這里讓我們回顧下Authentication到底是什么?Authentication是一個(gè)接口聲明,一個(gè)特定行為的聲明,它并不是一個(gè)類,沒有辦法實(shí)例化為對象進(jìn)行傳遞。所以我們首先需要對Authentication進(jìn)行實(shí)現(xiàn),使其可以被實(shí)例化。

在UsernamePasswordAuthenticationFilter的身份驗(yàn)證設(shè)計(jì)里,我們需要驗(yàn)證協(xié)議用簡單的語言可以描述為:給我一組用戶名和密碼,如果匹配,那么就算驗(yàn)證成功。用戶名即是一個(gè)唯一可以標(biāo)識不同用戶的字段,而密碼則是檢驗(yàn)當(dāng)前的身份驗(yàn)證是否正確的憑證信息。在Spring Security中便將使用username和password封裝成Authentication的實(shí)現(xiàn)聲明為了
UsernamePasswordAuthenticationToken繼承了抽象類,其主要與AbstractAuthenticationToken的區(qū)分就是針對使用用戶名和密碼驗(yàn)證的請求按照約定進(jìn)行了一定的封裝:將username賦值到了principal ,而將password賦值到了credentials。
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
通過UsernamePasswordAuthenticationToken實(shí)例化了Authentication接口,繼而按照流程,將其傳遞給AuthenticationMananger調(diào)用身份驗(yàn)證核心完成相關(guān)工作。
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
以上將來自HTTP請求中的參數(shù)按照預(yù)先約定放入賦值給Authentication指定屬性,便是UsernamePasswordAuthenticationFilter部分最主要的改動(dòng)。
驗(yàn)證核心的工作者:AuthenticationProvider
Web層的工作已經(jīng)完成了,Authentication接口的實(shí)現(xiàn)類UsernamePasswordAuthenticationToken通過AuthenticationMananger提供的驗(yàn)證方法作為參數(shù)被傳遞到了身份驗(yàn)證的核心組件中。
我們曾多次強(qiáng)調(diào)過一個(gè)設(shè)計(jì)概念:AuthenticationManager接口設(shè)計(jì)上并不是用于完成特定的身份驗(yàn)證工作的,而是調(diào)用其所配發(fā)的AuthenticationProvider接口去實(shí)現(xiàn)的。
那么這里就有一個(gè)疑問,針對接口聲明參數(shù)聲明的Authentication,針對不同驗(yàn)證協(xié)議的AuthenticationProvider的實(shí)現(xiàn)類們是完成對應(yīng)的工作的,并且AuthenticationManager是如何知道應(yīng)該使用哪一個(gè)AuthenticationProvider才能完成對應(yīng)協(xié)議的驗(yàn)證工作?
那么我們首先先復(fù)習(xí)下驗(yàn)證核心的大明星AuthenticationProvider接口的聲明:

AuthenticationProvider只包含兩個(gè)方法聲明,核心驗(yàn)證方法入口:
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
另外一個(gè)便是讓AuthenticationManager可以通過調(diào)用該方法辨別當(dāng)前AuthenticationProvider是否是完成相應(yīng)驗(yàn)證工作的supports方法:
boolean supports(Class<?> authentication);
簡單的描述便是AuthenticationProvider只有兩個(gè)方法,一個(gè)是它不能驗(yàn)證當(dāng)前的Authentication,還有便是讓他去驗(yàn)證當(dāng)前的Authentication。
對于AuthenticationProvider整個(gè)體系能說的非常多,本期只對我們“需要了解”的AuthenticationProvider中兩個(gè)接口聲明的方法做個(gè)最簡單的說明。其他部分在以后單獨(dú)對AuthenticationProvider體系介紹的時(shí)候再進(jìn)一步展開。
是否支持當(dāng)前驗(yàn)證協(xié)議:boolean supports(Class<?> authentication
在Spring Security中唯一AuthenticationManager的實(shí)現(xiàn)類,在處理authenticate身份驗(yàn)證入口方法的時(shí),首先第一解決的問題便是:我手下哪個(gè)AuthenticationProvider能驗(yàn)證當(dāng)前傳入的Authentication?為此ProviderManager便會(huì)對其所有的AuthenticationProvider做supports方法檢測,直到有AuthenticationProvider能在supports方法被調(diào)用后返回true。
我們了解了框架上的設(shè)計(jì)邏輯:先要知道知道誰能處理當(dāng)前的身份驗(yàn)證信息請求再要求它進(jìn)行驗(yàn)證工作。
回到我們的場景上來:UsernamePasswordAuthenticationFilter已經(jīng)封裝好了一個(gè)UsernamePasswordAuthenticationToken傳遞給了ProviderMananger。緊接著當(dāng)前ProviderMananger正焦頭爛額的詢問哪個(gè)AuthenticationProvider能支持這個(gè)Authentication的實(shí)現(xiàn)類。此時(shí)ProviderMananger所處的情況大概就跟下圖一般困惑:

在ProviderMananger的視角里,所有的Authentication實(shí)現(xiàn)類都不具名,它不僅不能通過自身完成驗(yàn)證工作也不能獨(dú)立完成判斷是否支持的工作,而是統(tǒng)統(tǒng)交給AuthenticationProvider去完成。而不同的AuthenticationProvider開發(fā)初衷本就是為了支持指定的某種驗(yàn)證協(xié)議,所以在特定的AuthenticationProvider的視角中,他只關(guān)心當(dāng)前Authentication是不是他預(yù)先設(shè)計(jì)處理的類型即可。
在使用用戶名和密碼的驗(yàn)證場景中,驗(yàn)證使用的用戶名和密碼被封裝成了UsernamePasswordAuthenticationToken對象。Spring Security便為了向UsernamePasswordAuthenticationToken對象在核心層提供相關(guān)的驗(yàn)證服務(wù)便繼承AuthenticationProvider開發(fā)了使用用戶名和密碼與UserDetailsService交互并且驗(yàn)證密碼的。
DaoAuthenticationProvider是的實(shí)現(xiàn)類,DaoAuthenticationProvider針對UsernamePasswordAuthenticationToken的大部分邏輯都是通過AbstractUserDetailsAuthenticationProvider完成的。比如針對ProviderManager詢問是否支持當(dāng)前Authentication的supports方法:
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
可能有些同學(xué)對isAssignableFrom方法比較陌生,這是一個(gè)判斷兩個(gè)類之間是否存在繼承關(guān)系使用的判斷方法,DaoAuthenticationProvider會(huì)判斷當(dāng)前的Authentication的實(shí)現(xiàn)類是否是UsernamePasswordAuthenticationToken它本身,或者是擴(kuò)展了UsernamePasswordAuthenticationToken的子孫類。返回true的場景只有一種,便是當(dāng)前的Authentication是UsernamePasswordAuthenticationToken實(shí)現(xiàn),換言之便是DaoAuthenticationProvider設(shè)計(jì)上需要進(jìn)行處理的某種特定的驗(yàn)證協(xié)議的信息載體的實(shí)現(xiàn)。
核心驗(yàn)證邏輯:Authentication authenticate(Authentication authentication)
完成了是否支持的supports驗(yàn)證后,ProviderMananger便會(huì)全權(quán)將驗(yàn)證工作交由DaoAuthenticationProvider進(jìn)行處理了。與ProviderMananger最不同一點(diǎn)是,在DaoAuthenticationProvider的視角里,當(dāng)前的Authentication最起碼一定是UsernamePasswordAuthenticationToken的形式了,不用和ProviderMananger一樣因?yàn)閰T乏信息而不知道干什么。
在DaoAuthenticationProvider分別會(huì)按照預(yù)先設(shè)計(jì)一樣分別從principal和credentials獲取用戶名和密碼進(jìn)行驗(yàn)證。
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
String presentedPassword = authentication.getCredentials().toString();
接著便是按照我們熟悉的預(yù)先設(shè)計(jì)流程,通過UserDetailsService使用username獲取對應(yīng)的UserDetails,最后通過對比密碼是否一致,向PrivoderManager返回最終的身份驗(yàn)證結(jié)果與身份信息。這樣一個(gè)特定場景使用用戶名和密碼的驗(yàn)證流程就完成了。
小結(jié)
我們先來總結(jié)下,當(dāng)前出現(xiàn)過的針對用戶名和密碼擴(kuò)展過的類與其為何被擴(kuò)展的原因。
- UsernamePasswordAuthenticationFilter擴(kuò)展AbstractAuthenticationProcessingFilter,因?yàn)樾枰獜腍TTP請求中從指定名稱的參數(shù)獲取用戶名和密碼,并且傳遞給驗(yàn)證核心;
- UsernamePasswordAuthenticationToken擴(kuò)展Authentication,因?yàn)槲覀冊O(shè)計(jì)了一套約定將用戶名和密碼放入了指定的屬性中以便核心讀取使用;
- DaoAuthenticationProvider 擴(kuò)展AuthenticationProvider,因?yàn)槲覀冃枰诤诵闹袑sernamePasswordAuthenticationToken進(jìn)行處理,并按照約定讀出用戶名和密碼使其可以進(jìn)行身份驗(yàn)證操作。

結(jié)尾
本章的重點(diǎn)是介紹特定場景下框架是如何通過擴(kuò)展指定組件來完成預(yù)設(shè)驗(yàn)證邏輯的交互過程。其實(shí)整個(gè)驗(yàn)證工作核心部分是在DaoAuthenticationProvider中進(jìn)行完成的,但是這部分內(nèi)容涉及到具體的驗(yàn)證協(xié)議的實(shí)現(xiàn)邏輯非常復(fù)雜,本期就暫時(shí)略過,在一下期中我們將對驗(yàn)證核心最重要的組件AuthenticationProvider其依賴的組件和對應(yīng)職責(zé)做一個(gè)全面的講解。
我們下期再見。