Spring Security 實現(xiàn)“記住我”功能及原理解析
這章繼續(xù)擴(kuò)展功能,來一個“記住我”的功能實現(xiàn),就是說用戶在登錄一次以后,系統(tǒng)會記住這個用戶一段時間,這段時間內(nèi)用戶不需要重新登錄就可以使用系統(tǒng)。
記住我功能基本原理
原理說明
- 用戶登錄發(fā)送認(rèn)證請求的時候會被
UsernamePasswordAuthenticationFilter
認(rèn)證攔截,認(rèn)證成功以后會調(diào)用一個RememberMeService
服務(wù),服務(wù)里面有一個TokenRepository
,這個服務(wù)會生成一個Token,然后將Token寫入到瀏覽器的Cookie同時會使用TokenRepository把生成的Token寫到數(shù)據(jù)庫里面,因為這個動作是在認(rèn)證成功以后做的,所以在Token寫入數(shù)據(jù)庫的時候會把用戶名同時寫入數(shù)據(jù)庫。 - 假如瀏覽器關(guān)了重新訪問系統(tǒng),用戶不需要再次登錄就可以訪問,這個時候請求在過濾器鏈上會經(jīng)過
RememberMeAuthenticationFilter
,這個過濾器的作用是讀取Cookie中的Token交給RemeberMeService,RemeberMeService會用TokenRepository到數(shù)據(jù)庫里去查這個Token在數(shù)據(jù)庫里有沒有記錄,如果有記錄就會把用戶名取出來,取出來以后會進(jìn)行各種校驗然后生成新Token再調(diào)用之前的UserDetailService
,去獲取用戶的信息,然后把用戶信息放到SecurityContext
里面,到這里就把用戶給登錄上了。
圖解說明
RememberMeAuthenticationFilter位于過濾器鏈的哪一環(huán)?
圖解
首先其他認(rèn)證過濾器會先進(jìn)行認(rèn)證,當(dāng)其他過濾器都無法認(rèn)證時,RememberMeAuthenticationFilter
會嘗試去做認(rèn)證。
記住我功能具體實現(xiàn)
前端頁面
登錄的時候加上一行記住我的勾選按鈕,這里要注意,name一定要是remember-me
,下面源碼部分會提到。
<tr> <td colspan='2'><input name="remember-me" type="checkbox" value="true" />記住我</td> </tr>
后臺
首先配置TokenRepository
Bean
/** * 記住我功能的Token存取器配置 * * @return */ @Bean public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); tokenRepository.setDataSource(dataSource); // 啟動的時候自動創(chuàng)建表,建表語句 JdbcTokenRepositoryImpl 已經(jīng)都寫好了 tokenRepository.setCreateTableOnStartup(true); return tokenRepository; }
然后需要在 configure 配置方法那邊進(jìn)行記住我功能所有組件的配置
protected void configure(HttpSecurity http) throws Exception { ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter(); http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) .formLogin() .loginPage("/authentication/require") .loginProcessingUrl("/authentication/form") .successHandler(meicloudAuthenticationSuccessHandler) .failureHandler(meicloudAuthenticationFailureHandler) // 配置記住我功能 .and() .rememberMe() // 配置TokenRepository .tokenRepository(persistentTokenRepository()) // 配置Token過期時間 .tokenValiditySeconds(3600) // 最終拿到用戶名之后,使用UserDetailsService去做登錄 .userDetailsService(userDetailsService) .and() .authorizeRequests() .antMatchers("/authentication/require", securityProperties.getBrowser().getSignInPage(), "/code/image").permitAll() .anyRequest() .authenticated() .and() .csrf().disable(); }
記住我功能Spring Security源碼解析
登錄之前“記住我”源碼流程
在認(rèn)證成功之后,會調(diào)用successfulAuthentication方法(這些第五章源碼部分已經(jīng)學(xué)習(xí)過),在將認(rèn)證信息保存到Context后,RememberMeServices就會調(diào)用它的loginSuccess方法
。
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { if (this.logger.isDebugEnabled()) { this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult); } SecurityContextHolder.getContext().setAuthentication(authResult); this.rememberMeServices.loginSuccess(request, response, authResult); if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); } this.successHandler.onAuthenticationSuccess(request, response, authResult); }
loginSuccess方法里面會先檢查請求中是否有name為remember-me
的參數(shù),有才進(jìn)行下一步。
public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { // this.parameter = "remember-me" if (!this.rememberMeRequested(request, this.parameter)) { this.logger.debug("Remember-me login not requested."); } else { this.onLoginSuccess(request, response, successfulAuthentication); } }
再進(jìn)入onLoginSuccess方法,里面主要就是進(jìn)行寫庫和寫Cookie
的操作。
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { String username = successfulAuthentication.getName(); this.logger.debug("Creating new persistent login for user " + username); // 生成Token PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date()); try { // 將Token和userName插入數(shù)據(jù)庫 this.tokenRepository.createNewToken(persistentToken); // 將Token寫到Cookie中 this.addCookie(persistentToken, request, response); } catch (Exception var7) { this.logger.error("Failed to save persistent token ", var7); } }
登錄之后“記住我”源碼流程
首先會進(jìn)入RememberMeAuthenticationFilter
,會先判斷前面的過濾器是否進(jìn)行過認(rèn)證(Context中是否有認(rèn)證信息),未進(jìn)行過認(rèn)證的話會調(diào)用RememberMeServices的autoLogin方法。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; if (SecurityContextHolder.getContext().getAuthentication() == null) { Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response); if (rememberMeAuth != null) { try { rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth); SecurityContextHolder.getContext().setAuthentication(rememberMeAuth); this.onSuccessfulAuthentication(request, response, rememberMeAuth); if (this.logger.isDebugEnabled()) { this.logger.debug("SecurityContextHolder populated with remember-me token: '" + SecurityContextHolder.getContext().getAuthentication() + "'"); } if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(SecurityContextHolder.getContext().getAuthentication(), this.getClass())); } if (this.successHandler != null) { this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth); return; } } catch (AuthenticationException var8) { if (this.logger.isDebugEnabled()) { this.logger.debug("SecurityContextHolder not populated with remember-me token, as AuthenticationManager rejected Authentication returned by RememberMeServices: '" + rememberMeAuth + "'; invalidating remember-me token", var8); } this.rememberMeServices.loginFail(request, response); this.onUnsuccessfulAuthentication(request, response, var8); } } chain.doFilter(request, response); } else { if (this.logger.isDebugEnabled()) { this.logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '" + SecurityContextHolder.getContext().getAuthentication() + "'"); } chain.doFilter(request, response); } }
autoLogin方法里面,主要調(diào)用this.processAutoLoginCookie(cookieTokens, request, response)
這個方法獲取數(shù)據(jù)庫中的用戶信息,其步驟是:
- 解析前端傳來的Cookie,里面包含了Token和seriesId,它會
使用seriesId查找數(shù)據(jù)庫的Token
- 檢查Cookie中的Token和數(shù)據(jù)庫查出來的Token
是否一樣
- 一樣的話再檢查數(shù)據(jù)庫中的Token
是否已過期
- 如果以上都符合的話,會使用舊的用戶名和series重新new一個Token,這時
過期時間也重新刷新
- 然后將新的Token保存回
數(shù)據(jù)庫
,同時添加回Cookie
中 - 最后再調(diào)用UserDetailsService的loadUserByUsername方法
返回UserDetails
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) { if (cookieTokens.length != 2) { throw new InvalidCookieException("Cookie token did not contain 2 tokens, but contained '" + Arrays.asList(cookieTokens) + "'"); } else { String presentedSeries = cookieTokens[0]; String presentedToken = cookieTokens[1]; PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries); if (token == null) { throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries); } else if (!presentedToken.equals(token.getTokenValue())) { this.tokenRepository.removeUserTokens(token.getUsername()); throw new CookieTheftException(this.messages.getMessage("PersistentTokenBasedRememberMeServices.cookieStolen", "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.")); } else if (token.getDate().getTime() + (long)this.getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) { throw new RememberMeAuthenticationException("Remember-me login has expired"); } else { if (this.logger.isDebugEnabled()) { this.logger.debug("Refreshing persistent login token for user '" + token.getUsername() + "', series '" + token.getSeries() + "'"); } PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), this.generateTokenData(), new Date()); try { this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate()); this.addCookie(newToken, request, response); } catch (Exception var9) { this.logger.error("Failed to update token: ", var9); throw new RememberMeAuthenticationException("Autologin failed due to data access problem"); } return this.getUserDetailsService().loadUserByUsername(token.getUsername()); } } }
回到RememberMeAuthenticationFilter,在調(diào)用了autoLogin方法之后得到了rememberMeAuth,然后再對其進(jìn)行一個認(rèn)證,認(rèn)證成功之后保存到SecurityContext
中,至此整個RememberMe自動登錄流程源碼結(jié)束。
相關(guān)閱讀:
Spring Security實現(xiàn)圖形驗證碼登錄
Spring Security實現(xiàn)短信驗證碼登錄
總結(jié)
到此這篇關(guān)于Spring Security 實現(xiàn)“記住我”功能及原理解析的文章就介紹到這了,更多相關(guān)spring security記住我內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java獲取兩個數(shù)組中不同數(shù)據(jù)的方法
這篇文章主要介紹了java獲取兩個數(shù)組中不同數(shù)據(jù)的方法,實例分析了java操作數(shù)組的技巧,非常具有實用價值,需要的朋友可以參考下2015-03-03SpringCloud Gateway的路由,過濾器和限流解讀
這篇文章主要介紹了SpringCloud Gateway的路由,過濾器和限流解讀,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-02-02