⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 http://niocoder.com/2018/01/17/Spring-Security源码分析七-Spring-Security-记住我/ 「龙飞」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

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

1. 记住我基本原理

http://dandandeshangni.oss-cn-beijing.aliyuncs.com/github/Spring%20Security/Spring-Security-remeber.png

  1. 用户认证成功之后调用RemeberMeService根据用户名名生成TokenTokenRepository写入到数据库,同时也将Token写入到浏览器的Cookie
  2. 重启服务之后,用户再次登入系统会由RememberMeAuthenticationFilter拦截,从Cookie中读取Token信息,与persistent_logins表匹配判断是否使用记住我功能。最中由UserDetailsService查询用户信息

1.1 记住我实现

  1. 创建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);

  2. 登录页面添加记住我复选款(name必须是remeber-me)

    <input name="remember-me" type="checkbox"> 下次自动登录

  3. 配置MerryyouSecurityConfig

    http.
    ......
    .and()
    .rememberMe()
    .tokenRepository(persistentTokenRepository())//设置操作表的Repository
    .tokenValiditySeconds(securityProperties.getRememberMeSeconds())//设置记住我的时间
    .userDetailsService(userDetailsService)//设置userDetailsService
    .and()
    ......

1.2 效果如下

https://raw.githubusercontent.com/longfeizheng/longfeizheng.github.io/master/images/security/spring-security-remeberme.gif

1.3 源码分析

1.3.1 首次登录

1.3.1.1 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);
}

  1. 将已认证过的Authentication放入到SecurityContext中
  2. 登录成功调用rememberMeServices

1.3.1.2 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);
}

  1. 判断是否勾选记住我

1.3.1.3 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);
}
}

  1. 获取用户名
  2. 创建Token
  3. 存储都数据库
  4. 写入到浏览器的Cookie中

1.3.2 二次登录Remember-me

1.3.2.1 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);
......

  1. 判断SecurityContext中没有Authentication
  2. 从Cookie查询用户信息返回RememberMeAuthenticationToken
  3. 如果不为空则由authenticationManager认证

1.3.2.2 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;
}

  1. 获取Cookie
  2. 解析Cookie
  3. 获取用户凭证
  4. 检查用户凭证

2. 代码下载

从我的 github 中下载,https://github.com/longfeizheng/logback

文章目录
  1. 1. 1. 记住我基本原理
    1. 1.1. 1.1 记住我实现
    2. 1.2. 1.2 效果如下
    3. 1.3. 1.3 源码分析
      1. 1.3.1. 1.3.1 首次登录
        1. 1.3.1.1. 1.3.1.1 AbstractAuthenticationProcessingFilter#successfulAuthentication
        2. 1.3.1.2. 1.3.1.2 AbstractRememberMeServices#loginSuccess
        3. 1.3.1.3. 1.3.1.3 PersistentTokenBasedRememberMeServices#onLoginSuccess
      2. 1.3.2. 1.3.2 二次登录Remember-me
        1. 1.3.2.1. 1.3.2.1 RememberMeAuthenticationFilter#doFilter
        2. 1.3.2.2. 1.3.2.2 AbstractRememberMeServices#autoLogin
  2. 2. 2. 代码下载