Spring Security 實現(xiàn)用戶名密碼登錄流程源碼詳解
引言
你在服務(wù)端的安全管理使用了 Spring Security,用戶登錄成功之后,Spring Security 幫你把用戶信息保存在 Session 里,但是具體保存在哪里,要是不深究你可能就不知道, 這帶來了一個問題,如果用戶在前端操作修改了當(dāng)前用戶信息,在不重新登錄的情況下,如何獲取到最新的用戶信息?
探究
無處不在的 Authentication
玩過 Spring Security 的小伙伴都知道,在 Spring Security 中有一個非常重要的對象叫做 Authentication,我們可以在任何地方注入 Authentication 進(jìn)而獲取到當(dāng)前登錄用戶信息,Authentication 本身是一個接口,它有很多實現(xiàn)類:
在這眾多的實現(xiàn)類中,我們最常用的就是 UsernamePasswordAuthenticationToken 了,但是當(dāng)我們打開這個類的源碼后,卻發(fā)現(xiàn)這個類平平無奇,他只有兩個屬性、兩個構(gòu)造方法以及若干個 get/set 方法;當(dāng)然,他還有更多屬性在它的父類上。
但是從它僅有的這兩個屬性中,我們也能大致看出,這個類就保存了我們登錄用戶的基本信息。那么我們的登錄信息是如何存到這兩個對象中的?這就要來梳理一下登錄流程了。
登錄流程
在 Spring Security 中,認(rèn)證與授權(quán)的相關(guān)校驗都是在一系列的過濾器鏈中完成的,在這一系列的過濾器鏈中,和認(rèn)證相關(guān)的過濾器就是 UsernamePasswordAuthenticationFilter::
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { //默認(rèn)的用戶名和密碼對應(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)前過濾器默認(rèn)攔截的路徑 private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login", "POST"); //默認(rèn)的請求參數(shù)名稱規(guī)定 private String usernameParameter = "username"; private String passwordParameter = "password"; //默認(rèn)只能是post請求 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 { //判斷請求方式 if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } else { //從請求參數(shù)中獲取對應(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); //校驗 //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ù)這段源碼我們可以看出:
首先通過 obtainUsername 和 obtainPassword 方法提取出請求里邊的用戶名/密碼出來,提取方式就是 request.getParameter ,這也是為什么 Spring Security 中默認(rèn)的表單登錄要通過 key/value 的形式傳遞參數(shù),而不能傳遞 JSON 參數(shù),如果像傳遞 JSON 參數(shù),修改這里的邏輯即可
獲取到請求里傳遞來的用戶名/密碼之后,接下來就構(gòu)造一個 UsernamePasswordAuthenticationToken
對象,傳入 username 和 password,username
對應(yīng)了 UsernamePasswordAuthenticationToken
中的 principal
屬性,而 password
則對應(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; } }
接下來 setDetails
方法給 details
屬性賦值,UsernamePasswordAuthenticationToken
本身是沒有 details
屬性的,這個屬性在它的父類 AbstractAuthenticationToken
中。details
是一個對象,這個對象里邊放的是 WebAuthenticationDetails
實例,該實例主要描述了兩個信息,請求的 remoteAddress
以及請求的 sessionId
。
最后一步,就是調(diào)用 authenticate 方法去做校驗了。
好了,從這段源碼中,大家可以看出來請求的各種信息基本上都找到了自己的位置,找到了位置,這就方便我們未來去獲取了。
接下來我們再來看請求的具體校驗操作。
校驗
在前面的 attemptAuthentication
方法中,該方法的最后一步開始做校驗,校驗操作首先要獲取到一個 AuthenticationManager
,這里拿到的是 ProviderManager
,所以接下來我們就進(jìn)入到 ProviderManager
的 authenticate
方法中,當(dāng)然這個方法也比較長,我這里僅僅摘列出來幾個重要的地方:
public Authentication authenticate(Authentication authentication) throws AuthenticationException { //獲取到主體(用戶名)和憑證(密碼)組成的一個令牌對象的class類對象 Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; AuthenticationException parentException = null; Authentication result = null; Authentication parentResult = null; int currentPosition = 0; //獲取所有可用來校驗令牌對象的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)前令牌對象的校驗 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)證校驗處理 result = provider.authenticate(authentication); //校驗成功返回一個新的authentication //將原先的主體由用戶名換成了userdetails對象 if (result != null) { //拷貝details到新的令牌對象 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)行驗證--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)的令牌對象 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; } }
這個方法就比較魔幻了,因為幾乎關(guān)于認(rèn)證的重要邏輯都將在這里完成:
首先獲取 authentication 的 Class,判斷當(dāng)前 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,結(jié)束 for 循環(huán),然后會進(jìn)入到下一個 if 中,直接調(diào)用 parent 的 authenticate 方法進(jìn)行校驗。
而 parent
就是 ProviderManager
,所以會再次回到這個 authenticate 方法中。再次回到 authenticate 方法中,provider 也變成了 DaoAuthenticationProvider,這個 provider 是支持 UsernamePasswordAuthenticationToken 的,所以會順利進(jìn)入到該類的 authenticate 方法去執(zhí)行,而 DaoAuthenticationProvider 繼承自 AbstractUserDetailsAuthenticationProvider 并且沒有重寫 authenticate 方法,所以 我們最終來到 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); //如果用戶沒有使用過,將其放進(jìn)緩存中 if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } return createSuccessAuthentication(principalToReturn, authentication, user); }
首先從 Authentication 提取出登錄用戶名。
然后通過拿著 username 去調(diào)用 retrieveUser 方法去獲取當(dāng)前用戶對象,這一步會調(diào)用我們自己在登錄時候的寫的 loadUserByUsername 方法,所以這里返回的 user 其實就是你的登錄對象
接下來調(diào)用 preAuthenticationChecks.check 方法去檢驗 user 中的各個賬戶狀態(tài)屬性是否正常,例如賬戶是否被禁用、賬戶是否被鎖定、賬戶是否過期等等
additionalAuthenticationChecks 方法則是做密碼比對的,好多小伙伴好奇 Spring Security 的密碼加密之后,是如何進(jìn)行比較的,看這里就懂了。
最后在 postAuthenticationChecks.check 方法中檢查密碼是否過期。
判斷用戶是否在緩存中存在,如果不存在,就放入緩存中
接下來有一個 forcePrincipalAsString 屬性,這個是是否強制將 Authentication 中的 principal 屬性設(shè)置為字符串,這個屬性我們一開始在 UsernamePasswordAuthenticationFilter 類中其實就是設(shè)置為字符串的(即 username),但是默認(rèn)情況下,當(dāng)用戶登錄成功之后, 這個屬性的值就變成當(dāng)前用戶這個對象了。之所以會這樣,就是因為 forcePrincipalAsString 默認(rèn)為 false,不過這塊其實不用改,就用 false,這樣在后期獲取當(dāng)前用戶信息的時候反而方便很多。
最后,通過 createSuccessAuthentication 方法構(gòu)建一個新的 UsernamePasswordAuthenticationToken,此時認(rèn)證主體就由用戶名變?yōu)榱藆serDetails對象
好了,那么登錄的校驗流程現(xiàn)在就基本和大家捋了一遍了。那么接下來還有一個問題,登錄的用戶信息我們?nèi)ツ睦锊檎遥?/p>
用戶信息保存
要去找登錄的用戶信息,我們得先來解決一個問題,就是上面我們說了這么多,這一切是從哪里開始被觸發(fā)的?
我們來到 UsernamePasswordAuthenticationFilter 的父類 AbstractAuthenticationProcessingFilter 中,這個類我們經(jīng)常會見到,因為很多時候當(dāng)我們想要在 Spring Security 自定義一個登錄驗證碼或者將登錄參數(shù)改為 JSON 的時候,我們都需自定義過濾器繼承自 AbstractAuthenticationProcessingFilter ,毫無疑問,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)的令牌對象 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)用時,實際上就是觸發(fā)了 UsernamePasswordAuthenticationFilter#attemptAuthentication 方法,當(dāng)?shù)卿洅伋霎惓5臅r候,unsuccessfulAuthentication 方法會被調(diào)用,而當(dāng)?shù)卿洺晒Φ臅r候,successfulAuthentication 方法則會被調(diào)用,那我們就來看一看 successfulAuthentication 方法:
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { //將新生產(chǎn)的令牌對象放入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);
,登錄成功的用戶信息被保存在這里,也就是說,在任何地方,如果我們想獲取用戶登錄信息,都可以從 SecurityContextHolder.getContext()
中獲取到,想修改,也可以在這里修改。
最后大家還看到有一個 successHandler.onAuthenticationSuccess
,這就是我們在 SecurityConfig 中配置登錄成功回調(diào)方法,就是在這里被觸發(fā)的
當(dāng)認(rèn)證失敗時,會調(diào)用登錄失敗處理器,并清空上下文環(huán)境中的對象
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 實現(xiàn)用戶名密碼登錄流程源碼詳解的詳細(xì)內(nèi)容,更多關(guān)于Spring Security 用戶名密碼登錄的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
java Springboot實現(xiàn)多文件上傳功能
這篇文章主要為大家詳細(xì)介紹了java Springboot實現(xiàn)多文件上傳功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-08-08Java同步關(guān)鍵字synchronize底層實現(xiàn)原理解析
synchronized關(guān)鍵字對大家來說并不陌生,當(dāng)我們遇到并發(fā)情況時,優(yōu)先會想到用synchronized關(guān)鍵字去解決,synchronized確實能夠幫助我們?nèi)ソ鉀Q并發(fā)的問題,接下來通過本文給大家分享java synchronize底層實現(xiàn)原理,感興趣的朋友一起看看吧2021-08-08Spring Boot詳細(xì)打印啟動時異常堆棧信息詳析
這篇文章主要給大家介紹了關(guān)于Spring Boot詳細(xì)打印啟動時異常堆棧信息的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用Spring Boot具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10Java?實戰(zhàn)項目之學(xué)生信息管理系統(tǒng)的實現(xiàn)流程
讀萬卷書不如行萬里路,只學(xué)書上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+SSM+jsp+mysql+maven實現(xiàn)學(xué)生信息管理系統(tǒng),大家可以在過程中查缺補漏,提升水平2021-11-11Spring結(jié)合WebSocket實現(xiàn)實時通信的教程詳解
WebSocket?是基于TCP/IP協(xié)議,獨立于HTTP協(xié)議的通信協(xié)議,本文將使用Spring結(jié)合WebSocket實現(xiàn)實時通信功能,有需要的小伙伴可以參考一下2024-01-01解決@Test注解在Maven工程的Test.class類中無法使用的問題
這篇文章主要介紹了解決@Test注解在Maven工程的Test.class類中無法使用的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03