使用者登入中常用的RememberMe-記住我功能,通俗來講,即使用者成功登入一次以後,系統自動記住該使用者一段時間(
可配置,Spring Security 框架預設為兩週
)。而在此時間段內,使用者不必重新登入即可訪問系統資源。本文即對
Spring Security
框架提供的
RememberMe-記住我
實現邏輯進行詳細講解,剖析其實現過程。
使用者登入
首先,在使用者登入時,如果使用者勾選了 記住我 選項,則系統會將該使用者的一些資訊進行處理,以便下次不必登入即可訪問系統。在第一次登入時,請求會由 UsernamePasswordAuthenticationFilter 攔截處理,進行身份認證。
認證成功後,會呼叫 RememberMeServices 的 loginSuccess 方法,處理成功登入的邏輯。
protectedvoidsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)throws IOException, ServletException {
……
rememberMeServices。loginSuccess(request, response, authResult);
……}
成功登入
使用者身份認證成功後,便由 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使用
使用者如果沒有退出登入,當再次訪問系統時,則不必再次登入(在有效期時間內)。這是由 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
框架採用了比較巧的方式來解決了。
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
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()); }}
原理圖
關於 RememberMe-記住我 的相關原理,上述已經進行了詳細的說明。不過,文字描述終歸不夠直觀,下面,再以圖示的方式來展示一下其執行原理。
其它詳細原始碼,請參考文末原始碼連結,可自行下載後閱讀。
原始碼
github
https://github。com/liuminglei/SpringSecurityLearning/tree/master/36
gitee
https://gitee。com/xbd521/SpringSecurityLearning/tree/master/36
- End -
回覆以下關鍵字,獲取更多資源
SpringCloud進階之路 | Java 基礎 | 微服務 | JAVA WEB | JAVA 進階 | JAVA 面試 | Java精講
往期精選
可能是最全的Thymeleaf參考手冊:終極篇,全,全,全!!!
ribbon,不帶這麼坑人的!
Spring Cloud進階之路:彙總篇
面試寶典(一):除零問題
重溫Java基礎(七):位運算子
避坑指南(四):zuul整合斷路器監控執行緒池一直loading
docker進階之路-基礎篇 | 二:protainer安裝與使用
叢集式Quartz定時任務框架實踐
如果喜歡我們的文章
可以關注我們
也可以點選右下角的在看告訴我們
期待與您相遇
點“在看”你懂得