SpringSecurity實現(xiàn)用戶名密碼登錄流程源碼詳解_第1頁
SpringSecurity實現(xiàn)用戶名密碼登錄流程源碼詳解_第2頁
SpringSecurity實現(xiàn)用戶名密碼登錄流程源碼詳解_第3頁
SpringSecurity實現(xiàn)用戶名密碼登錄流程源碼詳解_第4頁
SpringSecurity實現(xiàn)用戶名密碼登錄流程源碼詳解_第5頁
已閱讀5頁,還剩11頁未讀 繼續(xù)免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權,請進行舉報或認領

文檔簡介

第SpringSecurity實現(xiàn)用戶名密碼登錄流程源碼詳解目錄引言探究登錄流程校驗用戶信息保存

引言

你在服務端的安全管理使用了SpringSecurity,用戶登錄成功之后,SpringSecurity幫你把用戶信息保存在Session里,但是具體保存在哪里,要是不深究你可能就不知道,這帶來了一個問題,如果用戶在前端操作修改了當前用戶信息,在不重新登錄的情況下,如何獲取到最新的用戶信息?

探究

無處不在的Authentication

玩過SpringSecurity的小伙伴都知道,在SpringSecurity中有一個非常重要的對象叫做Authentication,我們可以在任何地方注入Authentication進而獲取到當前登錄用戶信息,Authentication本身是一個接口,它有很多實現(xiàn)類:

在這眾多的實現(xiàn)類中,我們最常用的就是UsernamePasswordAuthenticationToken了,但是當我們打開這個類的源碼后,卻發(fā)現(xiàn)這個類平平無奇,他只有兩個屬性、兩個構造方法以及若干個get/set方法;當然,他還有更多屬性在它的父類上。

但是從它僅有的這兩個屬性中,我們也能大致看出,這個類就保存了我們登錄用戶的基本信息。那么我們的登錄信息是如何存到這兩個對象中的?這就要來梳理一下登錄流程了。

登錄流程

在SpringSecurity中,認證與授權的相關校驗都是在一系列的過濾器鏈中完成的,在這一系列的過濾器鏈中,和認證相關的過濾器就是UsernamePasswordAuthenticationFilter::

publicclassUsernamePasswordAuthenticationFilterextendsAbstractAuthenticationProcessingFilter{

//默認的用戶名和密碼對應的key

publicstaticfinalStringSPRING_SECURITY_FORM_USERNAME_KEY="username";

publicstaticfinalStringSPRING_SECURITY_FORM_PASSWORD_KEY="password";

//當前過濾器默認攔截的路徑

privatestaticfinalAntPathRequestMatcherDEFAULT_ANT_PATH_REQUEST_MATCHER=newAntPathRequestMatcher("/login","POST");

//默認的請求參數(shù)名稱規(guī)定

privateStringusernameParameter="username";

privateStringpasswordParameter="password";

//默認只能是post請求

privatebooleanpostOnly=true;

publicUsernamePasswordAuthenticationFilter(){

//設置默認的攔截路徑

super(DEFAULT_ANT_PATH_REQUEST_MATCHER);

publicUsernamePasswordAuthenticationFilter(AuthenticationManagerauthenticationManager){

//設置默認的攔截路徑,和處理認證的管理器

super(DEFAULT_ANT_PATH_REQUEST_MATCHER,authenticationManager);

publicAuthenticationattemptAuthentication(HttpServletRequestrequest,HttpServletResponseresponse)throwsAuthenticationException{

//判斷請求方式

if(this.postOnly!request.getMethod().equals("POST")){

thrownewAuthenticationServiceException("Authenticationmethodnotsupported:"+request.getMethod());

}else{

//從請求參數(shù)中獲取對應的值

Stringusername=this.obtainUsername(request);

username=username!=nullusername:"";

username=username.trim();

Stringpassword=this.obtainPassword(request);

password=password!=nullpassword:"";

//構造用戶名和密碼登錄的認證令牌

UsernamePasswordAuthenticationTokenauthRequest=newUsernamePasswordAuthenticationToken(username,password);

//設置details---deltails里面默認存放sessionID和remoteaddr

//authRequest就是構造好的認證令牌

this.setDetails(request,authRequest);

//校驗

//authRequest就是構造好的認證令牌

returnthis.getAuthenticationManager().authenticate(authRequest);

@Nullable

protectedStringobtainPassword(HttpServletRequestrequest){

returnrequest.getParameter(this.passwordParameter);

@Nullable

protectedStringobtainUsername(HttpServletRequestrequest){

returnrequest.getParameter(this.usernameParameter);

protectedvoidsetDetails(HttpServletRequestrequest,UsernamePasswordAuthenticationTokenauthRequest){

authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));

publicvoidsetUsernameParameter(StringusernameParameter){

Assert.hasText(usernameParameter,"Usernameparametermustnotbeemptyornull");

this.usernameParameter=usernameParameter;

publicvoidsetPasswordParameter(StringpasswordParameter){

Assert.hasText(passwordParameter,"Passwordparametermustnotbeemptyornull");

this.passwordParameter=passwordParameter;

publicvoidsetPostOnly(booleanpostOnly){

this.postOnly=postOnly;

publicfinalStringgetUsernameParameter(){

returnthis.usernameParameter;

publicfinalStringgetPasswordParameter(){

returnthis.passwordParameter;

根據(jù)這段源碼我們可以看出:

首先通過obtainUsername和obtainPassword方法提取出請求里邊的用戶名/密碼出來,提取方式就是request.getParameter,這也是為什么SpringSecurity中默認的表單登錄要通過key/value的形式傳遞參數(shù),而不能傳遞JSON參數(shù),如果像傳遞JSON參數(shù),修改這里的邏輯即可

獲取到請求里傳遞來的用戶名/密碼之后,接下來就構造一個UsernamePasswordAuthenticationToken對象,傳入username和password,username對應了UsernamePasswordAuthenticationToken中的principal屬性,而password則對應了它的credentials屬性。

publicclassUsernamePasswordAuthenticationTokenextendsAbstractAuthenticationToken{

privatestaticfinallongserialVersionUID=550L;

privatefinalObjectprincipal;

privateObjectcredentials;

publicUsernamePasswordAuthenticationToken(Objectprincipal,Objectcredentials){

super((Collection)null);

this.principal=principal;

this.credentials=credentials;

this.setAuthenticated(false);

publicUsernamePasswordAuthenticationToken(Objectprincipal,Objectcredentials,CollectionextendsGrantedAuthorityauthorities){

super(authorities);

this.principal=principal;

this.credentials=credentials;

super.setAuthenticated(true);

publicObjectgetCredentials(){

returnthis.credentials;

publicObjectgetPrincipal(){

returnthis.principal;

publicvoidsetAuthenticated(booleanisAuthenticated)throwsIllegalArgumentException{

Assert.isTrue(!isAuthenticated,"Cannotsetthistokentotrusted-useconstructorwhichtakesaGrantedAuthoritylistinstead");

super.setAuthenticated(false);

publicvoideraseCredentials(){

super.eraseCredentials();

this.credentials=null;

接下來setDetails方法給details屬性賦值,UsernamePasswordAuthenticationToken本身是沒有details屬性的,這個屬性在它的父類AbstractAuthenticationToken中。details是一個對象,這個對象里邊放的是WebAuthenticationDetails實例,該實例主要描述了兩個信息,請求的remoteAddress以及請求的sessionId。

最后一步,就是調(diào)用authenticate方法去做校驗了。

好了,從這段源碼中,大家可以看出來請求的各種信息基本上都找到了自己的位置,找到了位置,這就方便我們未來去獲取了。

接下來我們再來看請求的具體校驗操作。

校驗

在前面的attemptAuthentication方法中,該方法的最后一步開始做校驗,校驗操作首先要獲取到一個AuthenticationManager,這里拿到的是ProviderManager,所以接下來我們就進入到ProviderManager的authenticate方法中,當然這個方法也比較長,我這里僅僅摘列出來幾個重要的地方:

publicAuthenticationauthenticate(Authenticationauthentication)throwsAuthenticationException{

//獲取到主體(用戶名)和憑證(密碼)組成的一個令牌對象的class類對象

ClassextendsAuthenticationtoTest=authentication.getClass();

AuthenticationExceptionlastException=null;

AuthenticationExceptionparentException=null;

Authenticationresult=null;

AuthenticationparentResult=null;

intcurrentPosition=0;

//獲取所有可用來校驗令牌對象的provider數(shù)量

intsize=viders.size();

//獲取迭代器

Iteratorvar9=this.getProviders().iterator();

//遍歷所有provider

while(var9.hasNext()){

AuthenticationProviderprovider=(AuthenticationProvider)var9.next();

//判斷當前provider是否支持當前令牌對象的校驗

if(provider.supports(toTest)){

if(logger.isTraceEnabled()){

Logvar10000=logger;

Stringvar10002=provider.getClass().getSimpleName();

++currentPosition;

var10000.trace(LogMessage.format("Authenticatingrequestwith%s(%d/%d)",var10002,currentPosition,size));

try{

//如果支持就進行認證校驗處理

result=provider.authenticate(authentication);

//校驗成功返回一個新的authentication

//將原先的主體由用戶名換成了userdetails對象

if(result!=null){

//拷貝details到新的令牌對象

this.copyDetails(authentication,result);

break;

}catch(InternalAuthenticationServiceException|AccountStatusExceptionvar14){

this.prepareException(var14,authentication);

throwvar14;

}catch(AuthenticationExceptionvar15){

lastException=var15;

//認證失敗但是provider的parent不為null

if(result==nullthis.parent!=null){

try{

//調(diào)用provider的parent進行驗證--parent就是providerManager

parentResult=this.parent.authenticate(authentication);

result=parentResult;

}catch(ProviderNotFoundExceptionvar12){

}catch(AuthenticationExceptionvar13){

parentException=var13;

lastException=var13;

//認證成功

if(result!=null){

//擦除憑證---密碼

if(this.eraseCredentialsAfterAuthenticationresultinstanceofCredentialsContainer){

((CredentialsContainer)result).eraseCredentials();

//發(fā)布認證成功的結果

if(parentResult==null){

this.eventPublisher.publishAuthenticationSuccess(result);

//返回新生產(chǎn)的令牌對象

returnresult;

}else{

//認證失敗

if(lastException==null){

lastException=newProviderNotFoundException(this.messages.getMessage("ProviderMviderNotFound",newObject[]{toTest.getName()},"NoAuthenticationProviderfoundfor{0}"));

if(parentException==null){

this.prepareException((AuthenticationException)lastException,authentication);

throwlastException;

這個方法就比較魔幻了,因為幾乎關于認證的重要邏輯都將在這里完成:

首先獲取authentication的Class,判斷當前provider是否支持該authentication。

如果支持,則調(diào)用provider的authenticate方法開始做校驗,校驗完成后,會返回一個新的Authentication。一會來和大家捋這個方法的具體邏輯

這里的provider可能有多個,如果provider的authenticate方法沒能正常返回一個Authentication,則調(diào)用provider的parent的authenticate方法繼續(xù)校驗。

copyDetails方法則用來把舊的Token的details屬性拷貝到新的Token中來。

接下來會調(diào)用eraseCredentials方法擦除憑證信息,也就是你的密碼,這個擦除方法比較簡單,就是將Token中的credentials屬性置空

最后通過publishAuthenticationSuccess方法將登錄成功的事件廣播出去。

大致的流程,就是上面這樣,在for循環(huán)中,第一次拿到的provider是一個AnonymousAuthenticationProvider,這個provider壓根就不支持UsernamePasswordAuthenticationToken,也就是會直接在provider.supports方法中返回false,結束for循環(huán),然后會進入到下一個if中,直接調(diào)用parent的authenticate方法進行校驗。

而parent就是ProviderManager,所以會再次回到這個authenticate方法中。再次回到authenticate方法中,provider也變成了DaoAuthenticationProvider,這個provider是支持UsernamePasswordAuthenticationToken的,所以會順利進入到該類的authenticate方法去執(zhí)行,而DaoAuthenticationProvider繼承自AbstractUserDetailsAuthenticationProvider并且沒有重寫authenticate方法,所以我們最終來到AbstractUserDetailsAuthenticationProvider#authenticate方法中:

publicAuthenticationauthenticate(Authenticationauthentication)

throwsAuthenticationException{

Stringusername=(authentication.getPrincipal()==null)"NONE_PROVIDED"

:authentication.getName();

user=retrieveUser(username,(UsernamePasswordAuthenticationToken)authentication);

preAuthenticationChecks.check(user);

additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken)authentication);

postAuthenticationChecks.check(user);

//如果用戶沒有使用過,將其放進緩存中

if(!cacheWasUsed){

this.userCache.putUserInCache(user);

ObjectprincipalToReturn=user;

if(forcePrincipalAsString){

principalToReturn=user.getUsername();

returncreateSuccessAuthentication(principalToReturn,authentication,user);

首先從Authentication提取出登錄用戶名。

然后通過拿著username去調(diào)用retrieveUser方法去獲取當前用戶對象,這一步會調(diào)用我們自己在登錄時候的寫的loadUserByUsername方法,所以這里返回的user其實就是你的登錄對象

接下來調(diào)用preAuthenticationChecks.check方法去檢驗user中的各個賬戶狀態(tài)屬性是否正常,例如賬戶是否被禁用、賬戶是否被鎖定、賬戶是否過期等等

additionalAuthenticationChecks方法則是做密碼比對的,好多小伙伴好奇SpringSecurity的密碼加密之后,是如何進行比較的,看這里就懂了。

最后在postAuthenticationChecks.check方法中檢查密碼是否過期。

判斷用戶是否在緩存中存在,如果不存在,就放入緩存中

接下來有一個forcePrincipalAsString屬性,這個是是否強制將Authentication中的principal屬性設置為字符串,這個屬性我們一開始在UsernamePasswordAuthenticationFilter類中其實就是設置為字符串的(即username),但是默認情況下,當用戶登錄成功之后,這個屬性的值就變成當前用戶這個對象了。之所以會這樣,就是因為forcePrincipalAsString默認為false,不過這塊其實不用改,就用false,這樣在后期獲取當前用戶信息的時候反而方便很多。

最后,通過createSuccessAuthentication方法構建一個新的UsernamePasswordAuthenticationToken,此時認證主體就由用戶名變?yōu)榱藆serDetails對象

好了,那么登錄的校驗流程現(xiàn)在就基本和大家捋了一遍了。那么接下來還有一個問題,登錄的用戶信息我們?nèi)ツ睦锊檎遥?/p>

用戶信息保存

要去找登錄的用戶信息,我們得先來解決一個問題,就是上面我們說了這么多,這一切是從哪里開始被觸發(fā)的?

我們來到UsernamePasswordAuthenticationFilter的父類AbstractAuthenticationProcessingFilter中,這個類我們經(jīng)常會見到,因為很多時候當我們想要在SpringSecurity自定義一個登錄驗證碼或者將登錄參數(shù)改為JSON的時候,我們都需自定義過濾器繼承自AbstractAuthenticationProcessingFilter,毫無疑問,UsernamePasswordAuthenticationFilter#attemptAuthentication方法就是在AbstractAuthenticationProcessingFilter類的doFilter方法中被觸發(fā)的:

privatevoiddoFilter(HttpServletRequestrequest,HttpServletResponseresponse,FilterChainchain)throwsIOException,ServletException{

//不需要認證就直接放行

if(!this.requiresAuthentication(request,response)){

chain.doFilter(request,response);

}else{

try{

//獲取認證的結果---null或者新生產(chǎn)的令牌對象

AuthenticationauthenticationResult=this.attemptAuthentication(request,response);

//認證失敗

if(authenticationResult==null){

return;

this.sessionStrategy.onAuthentication(authenticationResult,request,response);

if(this.continueChainBeforeSuccessfulAuthentication){

chain.doFilter(request,response);

this.successfulAuthentication(request,response,chain,authenticationResult);

}catch(InternalAuthenticationServiceExceptionvar5){

this.logger.error("Aninternalerroroccurredwhiletryingtoauthenticatetheuser.",var5);

this.unsuccessfulAuthentication(request,response,var5);

}catch(AuthenticationExceptionvar6){

this.unsuccessfulAuthentication(request,response,var6);

從上面的代碼中,我們可以看到,當attemptAuthentication方法被調(diào)用時,實際上就是觸發(fā)了UsernamePasswordAuthenticationFilter#attemptAuthentication方法,當?shù)卿洅伋霎惓5臅r候,unsuccessfulAuthentication方法會被調(diào)用,而當?shù)卿洺晒Φ臅r候,successfulAuthentication方法則會被調(diào)用,那我們就來看一看successfulAuthentication方法:

protectedvoidsuccessfulAuthentication(HttpServletRequestrequest,

HttpServletResponseresponse,FilterChainchain,A

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經(jīng)權益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負責。
  • 6. 下載文件中如有侵權或不適當內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論