Spring Security 實(shí)現(xiàn)“記住我”功能及原理解析
這章繼續(xù)擴(kuò)展功能,來(lái)一個(gè)“記住我”的功能實(shí)現(xiàn),就是說(shuō)用戶在登錄一次以后,系統(tǒng)會(huì)記住這個(gè)用戶一段時(shí)間,這段時(shí)間內(nèi)用戶不需要重新登錄就可以使用系統(tǒng)。
記住我功能基本原理
原理說(shuō)明
- 用戶登錄發(fā)送認(rèn)證請(qǐng)求的時(shí)候會(huì)被
UsernamePasswordAuthenticationFilter認(rèn)證攔截,認(rèn)證成功以后會(huì)調(diào)用一個(gè)RememberMeService服務(wù),服務(wù)里面有一個(gè)TokenRepository,這個(gè)服務(wù)會(huì)生成一個(gè)Token,然后將Token寫入到瀏覽器的Cookie同時(shí)會(huì)使用TokenRepository把生成的Token寫到數(shù)據(jù)庫(kù)里面,因?yàn)檫@個(gè)動(dòng)作是在認(rèn)證成功以后做的,所以在Token寫入數(shù)據(jù)庫(kù)的時(shí)候會(huì)把用戶名同時(shí)寫入數(shù)據(jù)庫(kù)。 - 假如瀏覽器關(guān)了重新訪問(wèn)系統(tǒng),用戶不需要再次登錄就可以訪問(wèn),這個(gè)時(shí)候請(qǐng)求在過(guò)濾器鏈上會(huì)經(jīng)過(guò)
RememberMeAuthenticationFilter,這個(gè)過(guò)濾器的作用是讀取Cookie中的Token交給RemeberMeService,RemeberMeService會(huì)用TokenRepository到數(shù)據(jù)庫(kù)里去查這個(gè)Token在數(shù)據(jù)庫(kù)里有沒(méi)有記錄,如果有記錄就會(huì)把用戶名取出來(lái),取出來(lái)以后會(huì)進(jìn)行各種校驗(yàn)然后生成新Token再調(diào)用之前的UserDetailService,去獲取用戶的信息,然后把用戶信息放到SecurityContext里面,到這里就把用戶給登錄上了。
圖解說(shuō)明

RememberMeAuthenticationFilter位于過(guò)濾器鏈的哪一環(huán)?
圖解

首先其他認(rèn)證過(guò)濾器會(huì)先進(jìn)行認(rèn)證,當(dāng)其他過(guò)濾器都無(wú)法認(rèn)證時(shí),RememberMeAuthenticationFilter會(huì)嘗試去做認(rèn)證。
記住我功能具體實(shí)現(xiàn)
前端頁(yè)面
登錄的時(shí)候加上一行記住我的勾選按鈕,這里要注意,name一定要是remember-me,下面源碼部分會(huì)提到。
<tr> <td colspan='2'><input name="remember-me" type="checkbox" value="true" />記住我</td> </tr>
后臺(tái)
首先配置TokenRepositoryBean
/**
* 記住我功能的Token存取器配置
*
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 啟動(dòng)的時(shí)候自動(dòng)創(chuàng)建表,建表語(yǔ)句 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過(guò)期時(shí)間
.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)證成功之后,會(huì)調(diào)用successfulAuthentication方法(這些第五章源碼部分已經(jīng)學(xué)習(xí)過(guò)),在將認(rèn)證信息保存到Context后,RememberMeServices就會(huì)調(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方法里面會(huì)先檢查請(qǐng)求中是否有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)行寫庫(kù)和寫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ù)庫(kù)
this.tokenRepository.createNewToken(persistentToken);
// 將Token寫到Cookie中
this.addCookie(persistentToken, request, response);
} catch (Exception var7) {
this.logger.error("Failed to save persistent token ", var7);
}
}
登錄之后“記住我”源碼流程
首先會(huì)進(jìn)入RememberMeAuthenticationFilter,會(huì)先判斷前面的過(guò)濾器是否進(jìn)行過(guò)認(rèn)證(Context中是否有認(rèn)證信息),未進(jìn)行過(guò)認(rèn)證的話會(huì)調(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)這個(gè)方法獲取數(shù)據(jù)庫(kù)中的用戶信息,其步驟是:
- 解析前端傳來(lái)的Cookie,里面包含了Token和seriesId,它會(huì)
使用seriesId查找數(shù)據(jù)庫(kù)的Token - 檢查Cookie中的Token和數(shù)據(jù)庫(kù)查出來(lái)的Token
是否一樣 - 一樣的話再檢查數(shù)據(jù)庫(kù)中的Token
是否已過(guò)期 - 如果以上都符合的話,會(huì)使用舊的用戶名和series重新new一個(gè)Token,這時(shí)
過(guò)期時(shí)間也重新刷新 - 然后將新的Token保存回
數(shù)據(jù)庫(kù),同時(shí)添加回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,然后再對(duì)其進(jìn)行一個(gè)認(rèn)證,認(rèn)證成功之后保存到SecurityContext中,至此整個(gè)RememberMe自動(dòng)登錄流程源碼結(jié)束。
相關(guān)閱讀:
Spring Security實(shí)現(xiàn)圖形驗(yàn)證碼登錄
Spring Security實(shí)現(xiàn)短信驗(yàn)證碼登錄
總結(jié)
到此這篇關(guān)于Spring Security 實(shí)現(xiàn)“記住我”功能及原理解析的文章就介紹到這了,更多相關(guān)spring security記住我內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java獲取兩個(gè)數(shù)組中不同數(shù)據(jù)的方法
這篇文章主要介紹了java獲取兩個(gè)數(shù)組中不同數(shù)據(jù)的方法,實(shí)例分析了java操作數(shù)組的技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-03-03
SpringBoot項(xiàng)目URL訪問(wèn)異常的問(wèn)題處理
這篇文章主要介紹了SpringBoot項(xiàng)目URL訪問(wèn)異常的問(wèn)題處理方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
eclipse實(shí)現(xiàn)ECDSA數(shù)字簽名
這篇文章主要為大家詳細(xì)介紹了eclipse實(shí)現(xiàn)ECDSA數(shù)字簽名,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-06-06
java中 Set與Map排序輸出到Writer詳解及實(shí)例
這篇文章主要介紹了 java中 Set與Map排序輸出到Writer詳解及實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-03-03
SpringCloud Gateway的路由,過(guò)濾器和限流解讀
這篇文章主要介紹了SpringCloud Gateway的路由,過(guò)濾器和限流解讀,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02
Java Maven高級(jí)之插件開(kāi)發(fā)詳解
這篇文章主要介紹了Maven 插件開(kāi)發(fā)的詳細(xì)整理的相關(guān)資料,需要的朋友可以看下,希望能夠給你帶來(lái)幫助2021-09-09

