有这样一个场景——有个用户初访并登录了你的网站,然而第二天他又来了,却必须再次登录。于是就有了“记住我”这样的功能来方便用户使用,然而有一件不言自明的事情,那就是这种认证状态的”旷日持久“早已超出了用户原本所需要的使用范围。这意味着,他们可以关闭浏览器,然后再关闭电脑,下周或者下个月,乃至更久以后再回来,只要这间隔时间不要太离谱,该网站总会知道谁是谁,并一如既往的为他们提供所有相同的功能和服务——与许久前他们离开的时候别无二致。
记住我基本原理
- 用户认证成功之后调用
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"> 下次自动登录
- 配置MerryyouSecurityConfig
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 中下载,https://github.com/longfeizheng/logback