Spring Security小教程 Vol 4. 使用使用者名稱和密碼驗證身份-UsernamePasswordAuthenticationFilter

廢柴大叔阿基拉發表於2019-03-25

前言

上一期我們分享了Spring Security是如何通過AbstractAuthenticationProcessingFilter向Web應用向基於HTTP、瀏覽器的請求提供身份驗證服務的。 這一次我們針對最常用,也是Spring Security預設在HTTP上使用的驗證過濾器UsernamePasswordAuthenticationFilter即基於使用者名稱和密碼的身份驗證過濾器是如何與核心進行互動進行展開說明。目的是希望讓大家對如何在Spring Security的核心上完成一個指定的身份驗證協議的擴充套件工作,已經涉及相關主要元件及其角色職責有個初步的瞭解。 這一期的內容如果有了前幾期對身份驗證核心的背景,相對來說比較的簡單,因為整個流程就是在原有的基礎上更加具體化了場景:身份驗證的資料來源是使用者提交的請求,驗證的憑證是使用者名稱和密碼。由於這樣的原因,這一期更像是對前幾期的一個綜合性的應用總結。

第四期 UsernamePasswordAuthenticationFilter

本期的任務清單

  1. Spring Security向Web應用提供支援的相關元件
  2. 瞭解UsernamePasswordAuthenticationFilter的職責和實現
  3. 瞭解UsernamePasswordAuthenticationToken封裝的身份驗證資訊

一. UsernamePasswordAuthenticationFilter

1.1 角色與職責

UsernamePasswordAuthenticationFilterAbstractAuthenticationProcessingFilter針對使用使用者名稱和密碼進行身份驗證而定製化的一個過濾器。 在一開始我們先通過下面的配圖來回憶一下我們的老朋友AbstractAuthenticationProcessingFilter的在框架中的角色與職責。

AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter在整個身份驗證的流程中主要處理的工作就是所有與Web資源相關的事情,並且將其封裝成Authentication物件,最後呼叫AuthenticationManager的驗證方法。以UsernamePasswordAuthenticationFilter的工作大致也是如此,只不過在這個場景下更加明確了Authentication物件的封裝資料的來源和形式:使用使用者名稱和密碼。

1.2 屬性與方法

接著我們再UsernamePasswordAuthenticationFilter的屬性和方法做一個快速的瞭解。 UsernamePasswordAuthenticationFilter繼承擴充套件了AbstractAuthenticationProcessingFilter,相對與AbstractAuthenticationProcessingFilter而言主要有以下幾個改動:

UsernamePasswordAuthenticationFilter的屬性中額外增加了username和password欄位

  1. 屬性中增加了username和password欄位;
  2. 強制的只對POST請求應用;
  3. 重寫了attemptAuthentication身份驗證入口方法。

關於增加username和password的動機十分容易明白,在從請求中獲取表單提交的使用者名稱和密碼欄位便會賦值到這兩個屬性中。 而重寫attemptAuthentication的方法主要是完了構建一個UsernamePasswordAuthenticationToken物件並且將其傳遞給AuthenticationMananger進行身份驗證。

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
		username, password);

// Allow subclasses to set the "details" property
setDetails(request, authRequest);

return this.getAuthenticationManager().authenticate(authRequest);
複製程式碼

二. UsernamePasswordAuthenticationToken

2.1. 封裝驗證請求資料的載體

UsernamePasswordAuthenticationToken是整個身份驗證流程封裝了身份驗證請求資料中的資料物件。 在UsernamePasswordAuthenticationFilter的屬性宣告中額外增加了username和password的動機很容易明白,即需要從HttpRequest中獲取對應的引數欄位,並將其封裝進Authentication中傳遞給AuthenticationManager進行身份驗證這裡讓我們回顧下Authentication到底是什麼?Authentication是一個介面宣告,一個特定行為的宣告,它並不是一個類,沒有辦法例項化為物件進行傳遞。所以我們首先需要對Authentication進行實現,使其可以被例項化。

Authentication介面宣告

UsernamePasswordAuthenticationFilter的身份驗證設計裡,我們需要驗證協議用簡單的語言可以描述為:給我一組使用者名稱和密碼,如果匹配,那麼就算驗證成功。使用者名稱即是一個唯一可以標識不同使用者的欄位,而密碼則是檢驗當前的身份驗證是否正確的憑證資訊。在Spring Security中便將使用username和password封裝成Authentication的實現宣告為了UsernamePasswordAuthenticationToken

2.2. 封裝使用者名稱和密碼

UsernamePasswordAuthenticationToken繼承了AbstractAuthenticationToken抽象類,其主要與AbstractAuthenticationToken的區分就是針對使用使用者名稱和密碼驗證的請求按照約定進行了一定的封裝:將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例項化了Authentication介面,繼而按照流程,將其傳遞給AuthenticationMananger呼叫身份驗證核心完成相關工作。

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
		username, password);

// Allow subclasses to set the "details" property
setDetails(request, authRequest);

return this.getAuthenticationManager().authenticate(authRequest);
複製程式碼

以上將來自HTTP請求中的引數按照預先約定放入賦值給Authentication指定屬性,便是UsernamePasswordAuthenticationFilter部分最主要的改動。

三. 驗證核心的工作者:AuthenticationProvider

Web層的工作已經完成了,Authentication介面的實現類UsernamePasswordAuthenticationToken通過AuthenticationMananger提供的驗證方法作為引數被傳遞到了身份驗證的核心元件中。 我們曾多次強調過一個設計概念:AuthenticationManager介面設計上並不是用於完成特定的身份驗證工作的,而是呼叫其所配發的AuthenticationProvider介面去實現的。 那麼這裡就有一個疑問,針對介面宣告引數宣告的Authentication,針對不同驗證協議的AuthenticationProvider的實現類們是完成對應的工作的,並且AuthenticationManager是如何知道應該使用哪一個AuthenticationProvider才能完成對應協議的驗證工作? 那麼我們首先先複習下驗證核心的大明星AuthenticationProvider介面的宣告:

AuthenticationProvider介面宣告
AuthenticationProvider只包含兩個方法宣告:

一個是用於驗證身份請求AuthenticationToken的authenticate方法:

Authentication authenticate(Authentication authentication)
	throws AuthenticationException;
複製程式碼

另外一個便是讓AuthenticationManager可以通過呼叫該方法辨別當前AuthenticationProvider是否是完成相應驗證工作的supports方法:

 boolean supports(Class<?> authentication);
複製程式碼

對於AuthenticationProvider整個體系能說的非常多,本期只對我們“需要了解”的AuthenticationProvider中兩個介面宣告的方法做個最簡單的說明。其他部分在以後單獨對AuthenticationProvider體系介紹的時候再進一步展開。

3.1 第一個方法- supports

在Spring Security中唯一AuthenticationManager的實現類ProviderManager,在處理authenticate身份驗證入口方法的時,首先第一解決的問題便是:我手下哪個AuthenticationProvider能驗證當前傳入的Authentication?為此ProviderManager便會對其所有的AuthenticationProvider做supports方法檢測,直到有AuthenticationProvider能在supports方法被呼叫後返回true。

我們瞭解了框架上的設計邏輯:先要知道知道誰能處理當前的身份驗證資訊請求再要求它進行驗證工作。 回到我們的場景上來:UsernamePasswordAuthenticationFilter已經封裝好了一個UsernamePasswordAuthenticationToken,並將它傳遞給了ProviderMananger。隨後ProviderMananger便會一次輪訓它管理的所有AuthenticationProvider,詢問是否有誰能支援這個Authentication的實現類。此時ProviderMananger所處的情況大概就跟下圖一般困惑:

ProviderMananger的煉獄生活

在ProviderMananger的視角里,所有的Authentication實現類都不具名,它不僅不能通過自身完成驗證工作也不能獨立完成判斷是否支援的工作,而是統統交給AuthenticationProvider去完成。而不同的AuthenticationProvider開發初衷本就是為了支援指定的某種驗證協議,所以在特定的AuthenticationProvider的視角中,他只關心當前Authentication是不是他預先設計處理的型別即可。 在使用使用者名稱和密碼的驗證場景中,驗證使用的使用者名稱和密碼被封裝成了UsernamePasswordAuthenticationToken物件。Spring Security便為了向UsernamePasswordAuthenticationToken物件在核心層提供相關的驗證服務便繼承AuthenticationProvider開發了使用使用者名稱和密碼與UserDetailsService互動並且驗證密碼的DaoAuthenticationProviderDaoAuthenticationProvider是AbstractUserDetailsAuthenticationProvider的實現類,DaoAuthenticationProvider針對UsernamePasswordAuthenticationToken的大部分邏輯都是通過AbstractUserDetailsAuthenticationProvider完成的。比如針對ProviderManager詢問是否支援當前Authentication的supports方法:

	public boolean supports(Class<?> authentication) {
		return (UsernamePasswordAuthenticationToken.class
				.isAssignableFrom(authentication));
	}

複製程式碼

可能有些同學對isAssignableFrom方法比較陌生,這是一個判斷兩個類之間是否存在繼承關係使用的判斷方法,DaoAuthenticationProvider會判斷當前的Authentication的實現類是否是UsernamePasswordAuthenticationToken它本身,或者是擴充套件了UsernamePasswordAuthenticationToken的子孫類。返回true的場景只有一種,便是當前的Authentication是UsernamePasswordAuthenticationToken實現,換言之便是DaoAuthenticationProvider設計上需要進行處理的某種特定的驗證協議的資訊載體的實現。

3.2 第二個方法- authenticate

完成了是否支援的supports驗證後,ProviderMananger便會全權將驗證工作交由DaoAuthenticationProvider進行處理了。與ProviderMananger最不同一點是,在DaoAuthenticationProvider的視角里,當前的Authentication最起碼一定是UsernamePasswordAuthenticationToken的形式了,不用和ProviderMananger一樣因為匱乏資訊而不知道幹什麼。 在DaoAuthenticationProvider分別會按照預先設計一樣分別從principal和credentials獲取使用者名稱和密碼進行驗證。

String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
				: authentication.getName();

String presentedPassword = authentication.getCredentials().toString();
複製程式碼

接著便是按照我們熟悉的預先設計流程,通過UserDetailsService使用username獲取對應的UserDetails,最後通過對比密碼是否一致,向PrivoderManager返回最終的身份驗證結果與身份資訊。這樣一個特定場景使用使用者名稱和密碼的驗證流程就完成了。

小結

我們先來總結下,當前出現過的針對使用者名稱和密碼擴充套件過的類與其為何被擴充套件的原因。

  1. UsernamePasswordAuthenticationFilter擴充套件AbstractAuthenticationProcessingFilter,因為需要從HTTP請求中從指定名稱的引數獲取使用者名稱和密碼,並且傳遞給驗證核心;
  2. UsernamePasswordAuthenticationToken擴充套件Authentication,因為我們設計了一套約定將使用者名稱和密碼放入了指定的屬性中以便核心讀取使用;
  3. DaoAuthenticationProvider 擴充套件AuthenticationProvider,因為我們需要在核心中對UsernamePasswordAuthenticationToken進行處理,並按照約定讀出使用者名稱和密碼使其可以進行身份驗證操作。

客製化驗證協議過程中涉及擴充套件的類

結尾

本章的重點是介紹特定場景下框架是如何通過擴充套件指定元件來完成預設驗證邏輯的互動過程。其實整個驗證工作核心部分是在DaoAuthenticationProvider中進行完成的,但是這部分內容涉及到具體的驗證協議的實現邏輯非常複雜,本期就暫時略過,在一下期中我們將對驗證核心最重要的元件AuthenticationProvider其依賴的元件和對應職責做一個全面的講解。 我們下期再見。

相關文章