有這樣一個場景——有個使用者初訪並登入了你的網站,然而第二天他又來了,卻必須再次登入。於是就有了“記住我”這樣的功能來方便使用者使用,然而有一件不言自明的事情,那就是這種認證狀態的”曠日持久“早已超出了使用者原本所需要的使用範圍。這意味著,他們可以關閉瀏覽器,然後再關閉電腦,下週或者下個月,乃至更久以後再回來,只要這間隔時間不要太離譜,該網站總會知道誰是誰,並一如既往的為他們提供所有相同的功能和服務——與許久前他們離開的時候別無二致。
記住我基本原理
- 使用者認證成功之後呼叫
RemeberMeService
根據使用者名稱名生成Token
由TokenRepository
寫入到資料庫,同時也將Token
寫入到瀏覽器的Cookie
中 - 重啟服務之後,使用者再次登入系統會由
RememberMeAuthenticationFilter
攔截,從Cookie
中讀取Token
資訊,與persistent_logins
表匹配判斷是否使用記住我功能。最中由UserDetailsService
查詢使用者資訊
記住我實現
- 建立
persistent_logins
表
create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null);
複製程式碼
- 登陸頁面新增記住我複選款(name必須是remeber-me)
<input name="remember-me" type="checkbox"> 下次自動登入
複製程式碼
http.
......
.and()
.rememberMe()
.tokenRepository(persistentTokenRepository())//設定操作表的Repository
.tokenValiditySeconds(securityProperties.getRememberMeSeconds())//設定記住我的時間
.userDetailsService(userDetailsService)//設定userDetailsService
.and()
......
複製程式碼
效果如下
原始碼分析
首次登入
AbstractAuthenticationProcessingFilter#successfulAuthentication
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
//# 1.將已認證過的Authentication放入到SecurityContext中
SecurityContextHolder.getContext().setAuthentication(authResult);
//# 2.登入成功呼叫rememberMeServices
rememberMeServices.loginSuccess(request, response, authResult);
// Fire event
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
successHandler.onAuthenticationSuccess(request, response, authResult);
}
複製程式碼
- 將已認證過的Authentication放入到SecurityContext中
- 登入成功呼叫rememberMeServices
AbstractRememberMeServices#loginSuccess
private String parameter = DEFAULT_PARAMETER;//remember-me
public final void loginSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication successfulAuthentication) {
// #1.判斷是否勾選記住我
if (!rememberMeRequested(request, parameter)) {
logger.debug("Remember-me login not requested.");
return;
}
onLoginSuccess(request, response, successfulAuthentication);
}
複製程式碼
- 判斷是否勾選記住我
PersistentTokenBasedRememberMeServices#onLoginSuccess
protected void onLoginSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication successfulAuthentication) {
//#1.獲取使用者名稱
String username = successfulAuthentication.getName();
logger.debug("Creating new persistent login for user " + username);
//#2.建立Token
PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
username, generateSeriesData(), generateTokenData(), new Date());
try {
//#3.儲存都資料庫
tokenRepository.createNewToken(persistentToken);
//#4.寫入到瀏覽器的Cookie中
addCookie(persistentToken, request, response);
}
catch (Exception e) {
logger.error("Failed to save persistent token ", e);
}
}
複製程式碼
- 獲取使用者名稱
- 建立Token
- 儲存都資料庫
- 寫入到瀏覽器的Cookie中
二次登入Remember-me
RememberMeAuthenticationFilter#doFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
//#1.判斷SecurityContext中沒有Authentication
if (SecurityContextHolder.getContext().getAuthentication() == null) {
//#2.從Cookie查詢使用者資訊返回RememberMeAuthenticationToken
Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
response);
if (rememberMeAuth != null) {
// Attempt authenticaton via AuthenticationManager
try {
//#3.如果不為空則由authenticationManager認證
rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);
// Store to SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
onSuccessfulAuthentication(request, response, rememberMeAuth);
......
複製程式碼
- 判斷SecurityContext中沒有Authentication
- 從Cookie查詢使用者資訊返回RememberMeAuthenticationToken
- 如果不為空則由authenticationManager認證
AbstractRememberMeServices#autoLogin
public final Authentication autoLogin(HttpServletRequest request,
HttpServletResponse response) {
//#1.獲取Cookie
String rememberMeCookie = extractRememberMeCookie(request);
if (rememberMeCookie == null) {
return null;
}
logger.debug("Remember-me cookie detected");
if (rememberMeCookie.length() == 0) {
logger.debug("Cookie was empty");
cancelCookie(request, response);
return null;
}
UserDetails user = null;
try {
//#2.解析Cookie
String[] cookieTokens = decodeCookie(rememberMeCookie);
//#3.獲取使用者憑證
user = processAutoLoginCookie(cookieTokens, request, response);
//#4.檢查使用者憑證
userDetailsChecker.check(user);
logger.debug("Remember-me cookie accepted");
//#5.返回Authentication
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);
return null;
}
複製程式碼
- 獲取Cookie
- 解析Cookie
- 獲取使用者憑證
- 檢查使用者憑證
程式碼下載
從我的 github 中下載,github.com/longfeizhen…