史上最簡單的Spring Security教程(三十七):RememberMe記住我原理剖析

使用者登入中常用的RememberMe-記住我功能,通俗來講,即使用者成功登入一次以後,系統自動記住該使用者一段時間(

可配置,Spring Security 框架預設為兩週

)。而在此時間段內,使用者不必重新登入即可訪問系統資源。本文即對

Spring Security

框架提供的

RememberMe-記住我

實現邏輯進行詳細講解,剖析其實現過程。

使用者登入

史上最簡單的Spring Security教程(三十七):RememberMe記住我原理剖析

首先,在使用者登入時,如果使用者勾選了 記住我 選項,則系統會將該使用者的一些資訊進行處理,以便下次不必登入即可訪問系統。在第一次登入時,請求會由 UsernamePasswordAuthenticationFilter 攔截處理,進行身份認證。

認證成功後,會呼叫 RememberMeServices 的 loginSuccess 方法,處理成功登入的邏輯。

protectedvoidsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)throws IOException, ServletException {

……

rememberMeServices。loginSuccess(request, response, authResult);

……}

成功登入

史上最簡單的Spring Security教程(三十七):RememberMe記住我原理剖析

使用者身份認證成功後,便由 RememberMeServices 介面來處理後續的成功登入邏輯。

voidloginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication);

在其抽象實現類 AbstractRememberMeServices 中,進行了預設的實現。

publicfinalvoidloginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {

if (!rememberMeRequested(request, parameter)) { logger。debug(“Remember-me login not requested。”);return; }

onLoginSuccess(request, response, successfulAuthentication);}

而抽象方法 onLoginSuccess 則顯得尤為重要,這需要其子類去實現。Spring Security 框架預設提供了兩個子類:TokenBasedRememberMeServices、PersistentTokenBasedRememberMeServices。

前面已經介紹過,

PersistentTokenBasedRememberMeServices

相比於

TokenBasedRememberMeServices

,採用了更加安全的實現方式,而不是如

TokenBasedRememberMeServices

一般,簡單的將使用者資訊,如使用者名稱、密碼等按照一定的規則加密後儲存在Cookie中。詳情可檢視文章

史上最簡單的Spring Security教程(三十五):RememberMe記住我之更安全的實現方式-PersistentTokenBasedRememberMeServices

TokenBasedRememberMeServices

中的 onLoginSuccess 邏輯如下。

publicvoid onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {

String username = retrieveUserName(successfulAuthentication);String password = retrievePassword(successfulAuthentication);

// If unable to find a username and password, just abort as// TokenBasedRememberMeServices is// unable to construct a valid token in this case。if (!StringUtils。hasLength(username)) { logger。debug(“Unable to retrieve username”);return; }

if (!StringUtils。hasLength(password)) { UserDetails user = getUserDetailsService()。loadUserByUsername(username); password = user。getPassword();

if (!StringUtils。hasLength(password)) { logger。debug(“Unable to obtain password for user: ” + username);return; } }

int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication); long expiryTime = System。currentTimeMillis();// SEC-949 expiryTime += 1000L * (tokenLifetime < 0 ? TWO_WEEKS_S : tokenLifetime);

String signatureValue = makeTokenSignature(expiryTime, username, password);

setCookie(newString[] { username, Long。toString(expiryTime), signatureValue }, tokenLifetime, request, response);

if (logger。isDebugEnabled()) { logger。debug(“Added remember-me cookie for user ‘” + username + “’, expiry: ‘” + newDate(expiryTime) + “’”); }}

即按照

username + ":" + expiryTime + ":" + Md5Hex(username + ":" + expiryTime + ":" + password + ":" + key)

規則,儲存到Cookie中,預設過期時間為

14天

這裡就能凸顯出來一個問題,使用者的密碼儲存在了Cookie中,即便被加了密。不過,如其類註釋所說,這適用於大部分的應用,並沒有什麼問題。

This is a basic remember-me implementation which is suitable for many applications。

PersistentTokenBasedRememberMeServices

中的 onLoginSuccess 邏輯如下。

protectedvoid onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {String username = successfulAuthentication。getName();

logger。debug(“Creating new persistent login for user ” + username);

PersistentRememberMeToken persistentToken = new PersistentRememberMeToken( username, generateSeriesData(), generateTokenData(), newDate());try { tokenRepository。createNewToken(persistentToken); addCookie(persistentToken, request, response); }catch (Exception e) { logger。error(“Failed to save persistent token ”, e); }}

同 TokenBasedRememberMeServices 一樣,也儲存相關資訊到Cookie中,只不過,沒有密碼等敏感資訊。除此之外,還將token儲存到了 tokenRepository 中,這將在後續根據使用者名稱查詢其token。

publicvoidcreateNewToken(PersistentRememberMeToken token) { getJdbcTemplate()。update(insertTokenSql, token。getUsername(), token。getSeries(), token。getTokenValue(), token。getDate());}

token使用

史上最簡單的Spring Security教程(三十七):RememberMe記住我原理剖析

使用者如果沒有退出登入,當再次訪問系統時,則不必再次登入(在有效期時間內)。這是由 RememberMeAuthenticationFilter 實現的。

publicvoiddoFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res;

if (SecurityContextHolder。getContext()。getAuthentication() == null) { Authentication rememberMeAuth = rememberMeServices。autoLogin(request, response);

如果當前使用者沒有登入,即 SecurityContextHolder 中當前使用者上下文不存在 Authentication。此時,便會呼叫自動登入邏輯,即 RememberMeServices 介面的 autoLogin 方法。

同 loginSuccess 方法一樣,在其抽象實現類 AbstractRememberMeServices 中,進行了預設的實現。

publicfinal Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) { String rememberMeCookie = extractRememberMeCookie(request);

if (rememberMeCookie == null) {returnnull; }

logger。debug(“Remember-me cookie detected”);

if (rememberMeCookie。length() == 0) { logger。debug(“Cookie was empty”); cancelCookie(request, response);returnnull; }

UserDetails user = null;

try { String[] cookieTokens = decodeCookie(rememberMeCookie); user = processAutoLoginCookie(cookieTokens, request, response); userDetailsChecker。check(user);

logger。debug(“Remember-me cookie accepted”);

return createSuccessfulAuthentication(request, user); }catch (CookieTheftException cte) { cancelCookie(request, response);throw cte; }catch (UsernameNotFoundException noUser) { logger。debug(“Remember-me login was valid but corresponding user not found。”, noUser); }catch (InvalidCookieException invalidCookie) { logger。debug(“Invalid remember-me cookie: ” + invalidCookie。getMessage()); }catch (AccountStatusException statusInvalid) { logger。debug(“Invalid UserDetails: ” + statusInvalid。getMessage()); }catch (RememberMeAuthenticationException e) { logger。debug(e。getMessage()); }

cancelCookie(request, response);returnnull;}

這段邏輯看似挺多,其實也沒多少內容。主要為

抽取Cookie

解析

Cookie

自動登入

成功登入

異常處理

抽取Cookie

解析

Cookie

異常處理

邏輯較為簡單,這裡不再贅述,感興趣的可以自行檢視原始碼分析。

自動登入,即 processAutoLoginCookie 方法,同樣的由兩個預設子類:TokenBasedRememberMeServices、PersistentTokenBasedRememberMeServices。

TokenBasedRememberMeServices

中的邏輯如下。

protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {

if (cookieTokens。length != 3) {thrownew InvalidCookieException(“Cookie token did not contain 3” + “ tokens, but contained ‘” + Arrays。asList(cookieTokens) + “’”); }

long tokenExpiryTime;

try { tokenExpiryTime = new Long(cookieTokens[1])。longValue(); }catch (NumberFormatException nfe) {thrownew InvalidCookieException(“Cookie token[1] did not contain a valid number (contained ‘” + cookieTokens[1] + “’)”); }

if (isTokenExpired(tokenExpiryTime)) {thrownew InvalidCookieException(“Cookie token[1] has expired (expired on ‘” + newDate(tokenExpiryTime) + “’; current time is ‘” + newDate() + “’)”); }

// Check the user exists。// Defer lookup until after expiry time checked, to possibly avoid expensive// database call。

UserDetails userDetails = getUserDetailsService()。loadUserByUsername( cookieTokens[0]);

// Check signature of token matches remaining details。// Must do this after user lookup, as we need the DAO-derived password。// If efficiency was a major issue, just add in a UserCache implementation,// but recall that this method is usually only called once per HttpSession - if// the token is valid,// it will cause SecurityContextHolder population, whilst if invalid, will cause// the cookie to be cancelled。String expectedTokenSignature = makeTokenSignature(tokenExpiryTime, userDetails。getUsername(), userDetails。getPassword());

if (!equals(expectedTokenSignature, cookieTokens[2])) {thrownew InvalidCookieException(“Cookie token[2] contained signature ‘” + cookieTokens[2] + “’ but expected ‘” + expectedTokenSignature + “’”); }

return userDetails;}

還是根據之前的儲存規則,進行反解析,得到使用者的相關資訊,校驗通過後,獲取使用者的詳細資訊並返回

PersistentTokenBasedRememberMeServices

中的邏輯如下。

protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {

if (cookieTokens。length != 2) {thrownew InvalidCookieException(“Cookie token did not contain ” + 2 + “ tokens, but contained ‘” + Arrays。asList(cookieTokens) + “’”); }

final String presentedSeries = cookieTokens[0]; final String presentedToken = cookieTokens[1];

PersistentRememberMeToken token = tokenRepository 。getTokenForSeries(presentedSeries);

if (token == null) {// No series match, so we can‘t authenticate using this cookiethrownew RememberMeAuthenticationException(“No persistent token found for series id: ” + presentedSeries); }

// We have a match for this user/series combinationif (!presentedToken。equals(token。getTokenValue())) {// Token doesn’t match series value。 Delete all logins for this user and throw// an exception to warn them。 tokenRepository。removeUserTokens(token。getUsername());

thrownew CookieTheftException( messages。getMessage(“PersistentTokenBasedRememberMeServices。cookieStolen”,“Invalid remember-me token (Series/token) mismatch。 Implies previous cookie theft attack。”)); }

if (token。getDate()。getTime() + getTokenValiditySeconds() * 1000L < System 。currentTimeMillis()) {thrownew RememberMeAuthenticationException(“Remember-me login has expired”); }

// Token also matches, so login is valid。 Update the token value, keeping the// *same* series number。if (logger。isDebugEnabled()) { logger。debug(“Refreshing persistent login token for user ‘” + token。getUsername() + “’, series ‘” + token。getSeries() + “’”); }

PersistentRememberMeToken newToken = new PersistentRememberMeToken( token。getUsername(), token。getSeries(), generateTokenData(), newDate());

try { tokenRepository。updateToken(newToken。getSeries(), newToken。getTokenValue(), newToken。getDate()); addCookie(newToken, request, response); }catch (Exception e) { logger。error(“Failed to update token: ”, e);thrownew RememberMeAuthenticationException(“Autologin failed due to data access problem”); }

return getUserDetailsService()。loadUserByUsername(token。getUsername());}

這裡的邏輯也比較簡單,不過,有一點為重中之重:全部校驗通過後,生成新token儲存到 tokenRepository 中;同時,將新的token儲存到Cookie中。

關於退出

史上最簡單的Spring Security教程(三十七):RememberMe記住我原理剖析

關於退出登入時的後續操作,其實,

Spring Security

框架採用了比較巧的方式來解決了。

LogoutFilter

會存在一系列的

LogoutHandler

,而

TokenBasedRememberMeServices

PersistentTokenBasedRememberMeServices

則預設實現了該介面。

publicabstractclassAbstractRememberMeServicesimplementsRememberMeServices,InitializingBean, LogoutHandler{

因此,無論最後

RememberMeServices

最後使用了那個實現,都會被初始化到

LogoutFilter

中,在使用者退出登入時,會自動執行 logout 方法。

public void init(H http) throws Exception { validateInput(); String key = getKey(); RememberMeServices rememberMeServices = getRememberMeServices(http, key); http。setSharedObject(RememberMeServices。class, rememberMeServices); LogoutConfigurer logoutConfigurer = http。getConfigurer(LogoutConfigurer。class);if (logoutConfigurer != null && this。logoutHandler != null) { logoutConfigurer。addLogoutHandler(this。logoutHandler); }

RememberMeAuthenticationProvider authenticationProvider = new RememberMeAuthenticationProvider( key); authenticationProvider = postProcess(authenticationProvider); http。authenticationProvider(authenticationProvider);

initDefaultLoginFilter(http);}

那麼,logout 方法到底都執行了哪些邏輯呢?

首先,

TokenBasedRememberMeServices

中的退出邏輯如下。

publicvoidlogout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {if (logger。isDebugEnabled()) { logger。debug(“Logout of user ” + (authentication == null ? “Unknown” : authentication。getName())); } cancelCookie(request, response);}

protectedvoidcancelCookie(HttpServletRequest request, HttpServletResponse response) { logger。debug(“Cancelling cookie”); Cookie cookie = new Cookie(cookieName, null); cookie。setMaxAge(0); cookie。setPath(getCookiePath(request));if (cookieDomain != null) { cookie。setDomain(cookieDomain); } response。addCookie(cookie);}

由於

TokenBasedRememberMeServices

主要就是將使用者資訊按照一定規則加密後儲存到Cookie中,所以,退出登入時,也只需簡單的清除Cookie即可。

由於

TokenBasedRememberMeServices

主要就是將使用者資訊按照一定規則加密後儲存到Cookie中,所以,退出登入時,也只需簡單的清除Cookie即可。

publicvoidlogout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {super。logout(request, response, authentication);

if (authentication != null) { tokenRepository。removeUserTokens(authentication。getName()); }}

原理圖

史上最簡單的Spring Security教程(三十七):RememberMe記住我原理剖析

關於 RememberMe-記住我 的相關原理,上述已經進行了詳細的說明。不過,文字描述終歸不夠直觀,下面,再以圖示的方式來展示一下其執行原理。

史上最簡單的Spring Security教程(三十七):RememberMe記住我原理剖析

其它詳細原始碼,請參考文末原始碼連結,可自行下載後閱讀。

原始碼

github

https://github。com/liuminglei/SpringSecurityLearning/tree/master/36

gitee

https://gitee。com/xbd521/SpringSecurityLearning/tree/master/36

史上最簡單的Spring Security教程(三十七):RememberMe記住我原理剖析

- End -

史上最簡單的Spring Security教程(三十七):RememberMe記住我原理剖析

回覆以下關鍵字,獲取更多資源

SpringCloud進階之路 | Java 基礎 | 微服務 | JAVA WEB | JAVA 進階 | JAVA 面試 | Java精講

史上最簡單的Spring Security教程(三十七):RememberMe記住我原理剖析

往期精選

史上最簡單的Spring Security教程(三十七):RememberMe記住我原理剖析

史上最簡單的Spring Security教程(三十七):RememberMe記住我原理剖析

可能是最全的Thymeleaf參考手冊:終極篇,全,全,全!!!

ribbon,不帶這麼坑人的!

Spring Cloud進階之路:彙總篇

面試寶典(一):除零問題

重溫Java基礎(七):位運算子

避坑指南(四):zuul整合斷路器監控執行緒池一直loading

docker進階之路-基礎篇 | 二:protainer安裝與使用

叢集式Quartz定時任務框架實踐

如果喜歡我們的文章

可以關注我們

也可以點選右下角的在看告訴我們

期待與您相遇

點“在看”你懂得

史上最簡單的Spring Security教程(三十七):RememberMe記住我原理剖析