Spring Security 實(shí)現(xiàn)用戶名密碼登錄流程源碼詳解
引言
你在服務(wù)端的安全管理使用了 Spring Security,用戶登錄成功之后,Spring Security 幫你把用戶信息保存在 Session 里,但是具體保存在哪里,要是不深究你可能就不知道, 這帶來(lái)了一個(gè)問(wèn)題,如果用戶在前端操作修改了當(dāng)前用戶信息,在不重新登錄的情況下,如何獲取到最新的用戶信息?
探究
無(wú)處不在的 Authentication
玩過(guò) Spring Security 的小伙伴都知道,在 Spring Security 中有一個(gè)非常重要的對(duì)象叫做 Authentication,我們可以在任何地方注入 Authentication 進(jìn)而獲取到當(dāng)前登錄用戶信息,Authentication 本身是一個(gè)接口,它有很多實(shí)現(xiàn)類:
在這眾多的實(shí)現(xiàn)類中,我們最常用的就是 UsernamePasswordAuthenticationToken 了,但是當(dāng)我們打開(kāi)這個(gè)類的源碼后,卻發(fā)現(xiàn)這個(gè)類平平無(wú)奇,他只有兩個(gè)屬性、兩個(gè)構(gòu)造方法以及若干個(gè) get/set 方法;當(dāng)然,他還有更多屬性在它的父類上。
但是從它僅有的這兩個(gè)屬性中,我們也能大致看出,這個(gè)類就保存了我們登錄用戶的基本信息。那么我們的登錄信息是如何存到這兩個(gè)對(duì)象中的?這就要來(lái)梳理一下登錄流程了。
登錄流程
在 Spring Security 中,認(rèn)證與授權(quán)的相關(guān)校驗(yàn)都是在一系列的過(guò)濾器鏈中完成的,在這一系列的過(guò)濾器鏈中,和認(rèn)證相關(guān)的過(guò)濾器就是 UsernamePasswordAuthenticationFilter::
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { //默認(rèn)的用戶名和密碼對(duì)應(yīng)的key public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username"; public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password"; //當(dāng)前過(guò)濾器默認(rèn)攔截的路徑 private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login", "POST"); //默認(rèn)的請(qǐng)求參數(shù)名稱規(guī)定 private String usernameParameter = "username"; private String passwordParameter = "password"; //默認(rèn)只能是post請(qǐng)求 private boolean postOnly = true; public UsernamePasswordAuthenticationFilter() { //設(shè)置默認(rèn)的攔截路徑 super(DEFAULT_ANT_PATH_REQUEST_MATCHER); } public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) { //設(shè)置默認(rèn)的攔截路徑,和處理認(rèn)證的管理器 super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager); } public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { //判斷請(qǐng)求方式 if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } else { //從請(qǐng)求參數(shù)中獲取對(duì)應(yīng)的值 String username = this.obtainUsername(request); username = username != null ? username : ""; username = username.trim(); String password = this.obtainPassword(request); password = password != null ? password : ""; //構(gòu)造用戶名和密碼登錄的認(rèn)證令牌 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); //設(shè)置details---deltails里面默認(rèn)存放sessionID和remoteaddr //authRequest 就是構(gòu)造好的認(rèn)證令牌 this.setDetails(request, authRequest); //校驗(yàn) //authRequest 就是構(gòu)造好的認(rèn)證令牌 return this.getAuthenticationManager().authenticate(authRequest); } } @Nullable protected String obtainPassword(HttpServletRequest request) { return request.getParameter(this.passwordParameter); } @Nullable protected String obtainUsername(HttpServletRequest request) { return request.getParameter(this.usernameParameter); } protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) { authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); } public void setUsernameParameter(String usernameParameter) { Assert.hasText(usernameParameter, "Username parameter must not be empty or null"); this.usernameParameter = usernameParameter; } public void setPasswordParameter(String passwordParameter) { Assert.hasText(passwordParameter, "Password parameter must not be empty or null"); this.passwordParameter = passwordParameter; } public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } public final String getUsernameParameter() { return this.usernameParameter; } public final String getPasswordParameter() { return this.passwordParameter; } }
根據(jù)這段源碼我們可以看出:
首先通過(guò) obtainUsername 和 obtainPassword 方法提取出請(qǐng)求里邊的用戶名/密碼出來(lái),提取方式就是 request.getParameter ,這也是為什么 Spring Security 中默認(rèn)的表單登錄要通過(guò) key/value 的形式傳遞參數(shù),而不能傳遞 JSON 參數(shù),如果像傳遞 JSON 參數(shù),修改這里的邏輯即可
獲取到請(qǐng)求里傳遞來(lái)的用戶名/密碼之后,接下來(lái)就構(gòu)造一個(gè) UsernamePasswordAuthenticationToken
對(duì)象,傳入 username 和 password,username
對(duì)應(yīng)了 UsernamePasswordAuthenticationToken
中的 principal
屬性,而 password
則對(duì)應(yīng)了它的 credentials
屬性。
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = 550L; private final Object principal; private Object credentials; public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { super((Collection)null); this.principal = principal; this.credentials = credentials; this.setAuthenticated(false); } public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); } public Object getCredentials() { return this.credentials; } public Object getPrincipal() { return this.principal; } public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { Assert.isTrue(!isAuthenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); super.setAuthenticated(false); } public void eraseCredentials() { super.eraseCredentials(); this.credentials = null; } }
接下來(lái) setDetails
方法給 details
屬性賦值,UsernamePasswordAuthenticationToken
本身是沒(méi)有 details
屬性的,這個(gè)屬性在它的父類 AbstractAuthenticationToken
中。details
是一個(gè)對(duì)象,這個(gè)對(duì)象里邊放的是 WebAuthenticationDetails
實(shí)例,該實(shí)例主要描述了兩個(gè)信息,請(qǐng)求的 remoteAddress
以及請(qǐng)求的 sessionId
。
最后一步,就是調(diào)用 authenticate 方法去做校驗(yàn)了。
好了,從這段源碼中,大家可以看出來(lái)請(qǐng)求的各種信息基本上都找到了自己的位置,找到了位置,這就方便我們未來(lái)去獲取了。
接下來(lái)我們?cè)賮?lái)看請(qǐng)求的具體校驗(yàn)操作。
校驗(yàn)
在前面的 attemptAuthentication
方法中,該方法的最后一步開(kāi)始做校驗(yàn),校驗(yàn)操作首先要獲取到一個(gè) AuthenticationManager
,這里拿到的是 ProviderManager
,所以接下來(lái)我們就進(jìn)入到 ProviderManager
的 authenticate
方法中,當(dāng)然這個(gè)方法也比較長(zhǎng),我這里僅僅摘列出來(lái)幾個(gè)重要的地方:
public Authentication authenticate(Authentication authentication) throws AuthenticationException { //獲取到主體(用戶名)和憑證(密碼)組成的一個(gè)令牌對(duì)象的class類對(duì)象 Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; AuthenticationException parentException = null; Authentication result = null; Authentication parentResult = null; int currentPosition = 0; //獲取所有可用來(lái)校驗(yàn)令牌對(duì)象的provider數(shù)量 int size = this.providers.size(); //獲取迭代器 Iterator var9 = this.getProviders().iterator(); //遍歷所有provider while(var9.hasNext()) { AuthenticationProvider provider = (AuthenticationProvider)var9.next(); //判斷當(dāng)前provider是否支持當(dāng)前令牌對(duì)象的校驗(yàn) if (provider.supports(toTest)) { if (logger.isTraceEnabled()) { Log var10000 = logger; String var10002 = provider.getClass().getSimpleName(); ++currentPosition; var10000.trace(LogMessage.format("Authenticating request with %s (%d/%d)", var10002, currentPosition, size)); } try { //如果支持就進(jìn)行認(rèn)證校驗(yàn)處理 result = provider.authenticate(authentication); //校驗(yàn)成功返回一個(gè)新的authentication //將原先的主體由用戶名換成了userdetails對(duì)象 if (result != null) { //拷貝details到新的令牌對(duì)象 this.copyDetails(authentication, result); break; } } catch (InternalAuthenticationServiceException | AccountStatusException var14) { this.prepareException(var14, authentication); throw var14; } catch (AuthenticationException var15) { lastException = var15; } } } //認(rèn)證失敗但是 provider 的 parent不為null if (result == null && this.parent != null) { try { //調(diào)用 provider 的 parent進(jìn)行驗(yàn)證--parent就是providerManager parentResult = this.parent.authenticate(authentication); result = parentResult; } catch (ProviderNotFoundException var12) { } catch (AuthenticationException var13) { parentException = var13; lastException = var13; } } //認(rèn)證成功 if (result != null) { //擦除憑證---密碼 if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) { ((CredentialsContainer)result).eraseCredentials(); } //發(fā)布認(rèn)證成功的結(jié)果 if (parentResult == null) { this.eventPublisher.publishAuthenticationSuccess(result); } //返回新生產(chǎn)的令牌對(duì)象 return result; } else { //認(rèn)證失敗 if (lastException == null) { lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}")); } if (parentException == null) { this.prepareException((AuthenticationException)lastException, authentication); } throw lastException; } }
這個(gè)方法就比較魔幻了,因?yàn)閹缀蹶P(guān)于認(rèn)證的重要邏輯都將在這里完成:
首先獲取 authentication 的 Class,判斷當(dāng)前 provider 是否支持該 authentication。
如果支持,則調(diào)用 provider 的 authenticate方法開(kāi)始做校驗(yàn),校驗(yàn)完成后,會(huì)返回一個(gè)新的Authentication。一會(huì)來(lái)和大家捋這個(gè)方法的具體邏輯
這里的 provider 可能有多個(gè),如果 provider 的 authenticate 方法沒(méi)能正常返回一個(gè)Authentication,則調(diào)用 provider 的 parent 的 authenticate 方法繼續(xù)校驗(yàn)。
copyDetails 方法則用來(lái)把舊的 Token 的 details 屬性拷貝到新的 Token 中來(lái)。
接下來(lái)會(huì)調(diào)用 eraseCredentials 方法擦除憑證信息,也就是你的密碼,這個(gè)擦除方法比較簡(jiǎn)單,就是將 Token 中的credentials 屬性置空
最后通過(guò) publishAuthenticationSuccess 方法將登錄成功的事件廣播出去。
大致的流程,就是上面這樣,在 for 循環(huán)中,第一次拿到的 provider 是一個(gè) AnonymousAuthenticationProvider,這個(gè) provider 壓根就不支持 UsernamePasswordAuthenticationToken,也就是會(huì)直接在 provider.supports 方法中返回 false,結(jié)束 for 循環(huán),然后會(huì)進(jìn)入到下一個(gè) if 中,直接調(diào)用 parent 的 authenticate 方法進(jìn)行校驗(yàn)。
而 parent
就是 ProviderManager
,所以會(huì)再次回到這個(gè) authenticate 方法中。再次回到 authenticate 方法中,provider 也變成了 DaoAuthenticationProvider,這個(gè) provider 是支持 UsernamePasswordAuthenticationToken 的,所以會(huì)順利進(jìn)入到該類的 authenticate 方法去執(zhí)行,而 DaoAuthenticationProvider 繼承自 AbstractUserDetailsAuthenticationProvider 并且沒(méi)有重寫(xiě) authenticate 方法,所以 我們最終來(lái)到 AbstractUserDetailsAuthenticationProvider#authenticate
方法中:
public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication); preAuthenticationChecks.check(user); additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication); postAuthenticationChecks.check(user); //如果用戶沒(méi)有使用過(guò),將其放進(jìn)緩存中 if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } return createSuccessAuthentication(principalToReturn, authentication, user); }
首先從 Authentication 提取出登錄用戶名。
然后通過(guò)拿著 username 去調(diào)用 retrieveUser 方法去獲取當(dāng)前用戶對(duì)象,這一步會(huì)調(diào)用我們自己在登錄時(shí)候的寫(xiě)的 loadUserByUsername 方法,所以這里返回的 user 其實(shí)就是你的登錄對(duì)象
接下來(lái)調(diào)用 preAuthenticationChecks.check 方法去檢驗(yàn) user 中的各個(gè)賬戶狀態(tài)屬性是否正常,例如賬戶是否被禁用、賬戶是否被鎖定、賬戶是否過(guò)期等等
additionalAuthenticationChecks 方法則是做密碼比對(duì)的,好多小伙伴好奇 Spring Security 的密碼加密之后,是如何進(jìn)行比較的,看這里就懂了。
最后在 postAuthenticationChecks.check 方法中檢查密碼是否過(guò)期。
判斷用戶是否在緩存中存在,如果不存在,就放入緩存中
接下來(lái)有一個(gè) forcePrincipalAsString 屬性,這個(gè)是是否強(qiáng)制將 Authentication 中的 principal 屬性設(shè)置為字符串,這個(gè)屬性我們一開(kāi)始在 UsernamePasswordAuthenticationFilter 類中其實(shí)就是設(shè)置為字符串的(即 username),但是默認(rèn)情況下,當(dāng)用戶登錄成功之后, 這個(gè)屬性的值就變成當(dāng)前用戶這個(gè)對(duì)象了。之所以會(huì)這樣,就是因?yàn)?forcePrincipalAsString 默認(rèn)為 false,不過(guò)這塊其實(shí)不用改,就用 false,這樣在后期獲取當(dāng)前用戶信息的時(shí)候反而方便很多。
最后,通過(guò) createSuccessAuthentication 方法構(gòu)建一個(gè)新的 UsernamePasswordAuthenticationToken,此時(shí)認(rèn)證主體就由用戶名變?yōu)榱藆serDetails對(duì)象
好了,那么登錄的校驗(yàn)流程現(xiàn)在就基本和大家捋了一遍了。那么接下來(lái)還有一個(gè)問(wèn)題,登錄的用戶信息我們?nèi)ツ睦锊檎遥?/p>
用戶信息保存
要去找登錄的用戶信息,我們得先來(lái)解決一個(gè)問(wèn)題,就是上面我們說(shuō)了這么多,這一切是從哪里開(kāi)始被觸發(fā)的?
我們來(lái)到 UsernamePasswordAuthenticationFilter 的父類 AbstractAuthenticationProcessingFilter 中,這個(gè)類我們經(jīng)常會(huì)見(jiàn)到,因?yàn)楹芏鄷r(shí)候當(dāng)我們想要在 Spring Security 自定義一個(gè)登錄驗(yàn)證碼或者將登錄參數(shù)改為 JSON 的時(shí)候,我們都需自定義過(guò)濾器繼承自 AbstractAuthenticationProcessingFilter ,毫無(wú)疑問(wèn),UsernamePasswordAuthenticationFilter#attemptAuthentication 方法就是在 AbstractAuthenticationProcessingFilter 類的 doFilter 方法中被觸發(fā)的:
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { //不需要認(rèn)證就直接放行 if (!this.requiresAuthentication(request, response)) { chain.doFilter(request, response); } else { try { //獲取認(rèn)證的結(jié)果---null或者新生產(chǎn)的令牌對(duì)象 Authentication authenticationResult = this.attemptAuthentication(request, response); //認(rèn)證失敗 if (authenticationResult == null) { return; } this.sessionStrategy.onAuthentication(authenticationResult, request, response); if (this.continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } this.successfulAuthentication(request, response, chain, authenticationResult); } catch (InternalAuthenticationServiceException var5) { this.logger.error("An internal error occurred while trying to authenticate the user.", var5); this.unsuccessfulAuthentication(request, response, var5); } catch (AuthenticationException var6) { this.unsuccessfulAuthentication(request, response, var6); } } }
從上面的代碼中,我們可以看到,當(dāng) attemptAuthentication 方法被調(diào)用時(shí),實(shí)際上就是觸發(fā)了 UsernamePasswordAuthenticationFilter#attemptAuthentication 方法,當(dāng)?shù)卿洅伋霎惓5臅r(shí)候,unsuccessfulAuthentication 方法會(huì)被調(diào)用,而當(dāng)?shù)卿洺晒Φ臅r(shí)候,successfulAuthentication 方法則會(huì)被調(diào)用,那我們就來(lái)看一看 successfulAuthentication 方法:
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { //將新生產(chǎn)的令牌對(duì)象放入spring security的上下文環(huán)境中 SecurityContextHolder.getContext().setAuthentication(authResult); rememberMeServices.loginSuccess(request, response, authResult); // Fire event if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } successHandler.onAuthenticationSuccess(request, response, authResult); }
在這里有一段很重要的代碼,就是 SecurityContextHolder.getContext().setAuthentication(authResult);
,登錄成功的用戶信息被保存在這里,也就是說(shuō),在任何地方,如果我們想獲取用戶登錄信息,都可以從 SecurityContextHolder.getContext()
中獲取到,想修改,也可以在這里修改。
最后大家還看到有一個(gè) successHandler.onAuthenticationSuccess
,這就是我們?cè)?SecurityConfig 中配置登錄成功回調(diào)方法,就是在這里被觸發(fā)的
當(dāng)認(rèn)證失敗時(shí),會(huì)調(diào)用登錄失敗處理器,并清空上下文環(huán)境中的對(duì)象
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
this.logger.trace("Failed to process authentication request", failed);
this.logger.trace("Cleared SecurityContextHolder");
this.logger.trace("Handling authentication failure");
this.rememberMeServices.loginFail(request, response);
this.failureHandler.onAuthenticationFailure(request, response, failed);
}
以上就是Spring Security 實(shí)現(xiàn)用戶名密碼登錄流程源碼詳解的詳細(xì)內(nèi)容,更多關(guān)于Spring Security 用戶名密碼登錄的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring的CorsFilter會(huì)失效的原因及解決方法
眾所周知CorsFilter是Spring提供的跨域過(guò)濾器,我們可能會(huì)做以下的配置,基本上就是允許任何跨域請(qǐng)求,我利用Spring的CorsFilter做跨域操作但是出現(xiàn)報(bào)錯(cuò),接下來(lái)小編就給大家介紹一Spring的CorsFilter會(huì)失效的原因及解決方法,需要的朋友可以參考下2023-09-09Dom4j解析xml復(fù)雜多節(jié)點(diǎn)報(bào)文方式
這篇文章主要介紹了Dom4j解析xml復(fù)雜多節(jié)點(diǎn)報(bào)文方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09java Springboot實(shí)現(xiàn)多文件上傳功能
這篇文章主要為大家詳細(xì)介紹了java Springboot實(shí)現(xiàn)多文件上傳功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08Java同步關(guān)鍵字synchronize底層實(shí)現(xiàn)原理解析
synchronized關(guān)鍵字對(duì)大家來(lái)說(shuō)并不陌生,當(dāng)我們遇到并發(fā)情況時(shí),優(yōu)先會(huì)想到用synchronized關(guān)鍵字去解決,synchronized確實(shí)能夠幫助我們?nèi)ソ鉀Q并發(fā)的問(wèn)題,接下來(lái)通過(guò)本文給大家分享java synchronize底層實(shí)現(xiàn)原理,感興趣的朋友一起看看吧2021-08-08Spring Boot詳細(xì)打印啟動(dòng)時(shí)異常堆棧信息詳析
這篇文章主要給大家介紹了關(guān)于Spring Boot詳細(xì)打印啟動(dòng)時(shí)異常堆棧信息的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Spring Boot具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10Java?實(shí)戰(zhàn)項(xiàng)目之學(xué)生信息管理系統(tǒng)的實(shí)現(xiàn)流程
讀萬(wàn)卷書(shū)不如行萬(wàn)里路,只學(xué)書(shū)上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實(shí)戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+SSM+jsp+mysql+maven實(shí)現(xiàn)學(xué)生信息管理系統(tǒng),大家可以在過(guò)程中查缺補(bǔ)漏,提升水平2021-11-11Spring結(jié)合WebSocket實(shí)現(xiàn)實(shí)時(shí)通信的教程詳解
WebSocket?是基于TCP/IP協(xié)議,獨(dú)立于HTTP協(xié)議的通信協(xié)議,本文將使用Spring結(jié)合WebSocket實(shí)現(xiàn)實(shí)時(shí)通信功能,有需要的小伙伴可以參考一下2024-01-01解決@Test注解在Maven工程的Test.class類中無(wú)法使用的問(wèn)題
這篇文章主要介紹了解決@Test注解在Maven工程的Test.class類中無(wú)法使用的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03