Spring Security实现记住我功能&源码分析

Spring Security 实现“记住我”功能,即自动登录功能有两种方式:

  • 将 token写入到浏览器的 Cookie中
  • 将 token持久化到数据库

一、将 token写入到浏览器的 Cookie中

1、代码实现

1.1 后端
Spring Security默认是没有开启“记住我”功能,我们在 Spring Security配置类中开启它即可。

// 开启记住我功能
.rememberMe()
.key("rememberMeKey") // 默认 key为UUID,我们自定义 key
.tokenValiditySeconds(60) //设置token的过期时间,默认2周

注意:key 默认值是一个 UUID 字符串,如果服务端重启,这个 key 会变,这样会导致之前所有 remember-me 自动登录令牌失效,所以,我们一般都指定 key值。

1.2 前端
前端需要注意:

  • 记住我的字段名称 默认是 remember-me
  • remember-me的值必须是true | on | yes | "1"

这些字段名可以自定义,我们使用默认值就行。

1.3 测试
测试一下,登录认证通过后,关掉浏览器,再次打开页面,remember-me功能生效了,就这么简单。
在这里插入图片描述
我们将 remember-me的值,通过 Base64 转码后的字符串,得到:
在这里插入图片描述
可以看到,cookie 中 remember-me 的使用用 : 隔开,分成了三部分:

  1. 用户名。
  2. 时间戳,即 token的过期时间。
  3. 是使用 MD5 散列函数算出来的值,它的明文格式是 username + “:” + tokenExpiryTime + “:” + password + “:” + key,最后的 key 是一个散列盐值,可以用来防治令牌被修改。

2、源码分析

前面分析登录认证流程时,认证成功就会调用“记住我”功能。

查看 UsernamePasswordAuthenticationFilter的父类 AbstractAuthenticationProcessingFilter过滤器的 doFIlter方法中,认证做了两个分支,

  • 成功执行 successfulAuthentication,
  • 失败执行 unsuccessfulAuthentication。

在 successfulAuthentication内部,将用户认证信息存储到了 SecurityContext中,并调用了 loginSuccess方法,这就是“记住我”功能的核心方法。
在这里插入图片描述

2.1 token的生成

查看 AbstractRememberMeServices类的 loginSuccess方法

	private String parameter = "remember-me";    

	public final void loginSuccess(HttpServletRequest request, HttpServletResponse response,
                                   Authentication successfulAuthentication) {
		// 判断是否勾选记住我
		// 注意:这里this.parameter点进去是上面的private String parameter = "remember-me";
        if (!this.rememberMeRequested(request, this.parameter)) {
            this.logger.debug("Remember-me login not requested.");
        } else {
			//若勾选就调用onLoginSuccess方法
            this.onLoginSuccess(request, response, successfulAuthentication);
        }
    }

1)“记住我”表单属性的名称和值

        protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
            if (this.alwaysRemember) {
                return true;
            } else {
                // "remember-me"属性名默认为"remember-me"
                String paramValue = request.getParameter(parameter);
                // 这属性值可以为:true,on,yes,1。
                if (paramValue != null && (paramValue.equalsIgnoreCase("true") ||
                        paramValue.equalsIgnoreCase("on") || paramValue.equalsIgnoreCase("yes") ||
                        paramValue.equals("1"))) {
                    //满足上面条件才能返回true
                    return true;
                } else {
                    if (this.logger.isDebugEnabled()) {
                        this.logger.debug("Did not send remember-me cookie (principal did not set
                                parameter '" + parameter + "')");
                    }
                    return false;
                }
            }
        }

2)查看 TokenBasedRememberMeServices类的 onLoginSuccess方法。
在这里插入图片描述

public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
        //1.获取用户信息
        String username = this.retrieveUserName(successfulAuthentication);
        String password = this.retrievePassword(successfulAuthentication);
        if (!StringUtils.hasLength(username)) {
            this.logger.debug("Unable to retrieve username");
        } else {
            if (!StringUtils.hasLength(password)) {
                UserDetails user = this.getUserDetailsService().loadUserByUsername(username);
                password = user.getPassword();
                if (!StringUtils.hasLength(password)) {
                    this.logger.debug("Unable to obtain password for user: " + username);
                    return;
                }
            }
			//2.获取token的有效期
            int tokenLifetime = this.calculateLoginLifetime(request, successfulAuthentication);
            long expiryTime = System.currentTimeMillis();
            expiryTime += 1000L * (long)(tokenLifetime < 0 ? 1209600 : tokenLifetime);
             //3.生成MD5签名值
            String signatureValue = this.makeTokenSignature(expiryTime, username, password);
            //4.将信息添加到浏览器的 Cookie中
            this.setCookie(new String[]{username, Long.toString(expiryTime), signatureValue}, tokenLifetime, request, response);
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Added remember-me cookie for user '" + username + "', expiry: '" + new Date(expiryTime) + "'");
            }

        }
    }  

方法的逻辑如下:

  • 1、获取用户信息
    从登录成功的 Authentication 中提取出用户名/密码。
    由于登录成功之后,密码可能被擦除了,所以,如果一开始没有拿到密码,就再从 UserDetailsService 中重新加载用户并重新获取密码。
  • 2、获取token的有效期,令牌有效期默认就是两周。
  • 3、生成MD5签名值
    调用 makeTokenSignature 方法去计算散列值,实际上就是根据 username、令牌有效期以及 password、key 一起计算一个散列值。如果我们没有自己去设置这个 key,默认是在 RememberMeConfigurer#getKey 方法中进行设置的,它的值是一个 UUID 字符串。
  • 4、将用户名、令牌有效期以及计算得到的散列值,,生成 token值 并添加到浏览器的 Cookie中。

在这里插入图片描述

2.2 token解析

查看 RememberMeAuthenticationFilter过滤器的 doFilter 方法。
在这里插入图片描述
如果从 SecurityContextHolder 中无法获取到当前登录用户实例,就调用 rememberMeServices.autoLogin重点方法进行自动登录逻辑。

查看 autoLogin方法:
在这里插入图片描述
逻辑如下:

  • 1、提取出 cookie 信息,
  • 2、对 cookie 信息进行解码,
  • 3、调用 processAutoLoginCookie方法校验token,核心流程:首先获取用户名和过期时间,再根据用户名查询到用户密码,然后通过 MD5 散列函数计算出散列值,再将拿到的散列值和浏览器传递来的散列值进行对比,就能确认这个令牌是否有效,进而确认登录是否有效。
  • 4、用户状态判断
  • 5、创建 RememberMeAuthenticationToken实例

在这里插入图片描述
在这里插入图片描述

二、将 token持久化到数据库

“记住我”功能将 token保存到浏览器中,很容易盗取,而且 cookie的值还与用户名、密码这些敏感数据相关,虽然加密了,但是将敏感信息存在客户端,还是不太安全。

上面源码我们也看到了,认证成功之后,有两个“记住我”功能实现方式。
在这里插入图片描述
所以,Spring Security还提供了 remember me的另一种相对更安全的实现机制:将 token持久化到数据库。

在客户端的 cookie中,仅保存一个无意义的加密串(与用户名、密码等敏感数据无关),
然后在数据库中保存该加密串与用户信息的对应关系,
自动登录时,用cookie中的加密串,到数据库中验证,如果通过,自动登录才算通过。

创建记录 remember me一张表来记录令牌信息:

CREATE TABLE `persistent_logins` (
  `username` varchar(64) NOT NULL,
  `series` varchar(64) NOT NULL,
  `token` varchar(64) NOT NULL,
  `last_used` timestamp NOT NULL,
  PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

注意:这张表的名称和字段都是固定的,不要修改,官方提供的。如果使用默认的 JDBC,即 JdbcTokenRepositoryImpl。
这张表我们也可以完全自定义,也可以使用系统提供的 JDBC 来操作。这里我们使用官方的表和默认的 JDBC来实现“记住我”功能。

1、代码实现

1.1 后端
在 Spring Security配置类中开启它即可。并默认的 JDBC,指定 JdbcTokenRepositoryImpl。

    // 2.指定数据源
    @Autowired
    private DataSource dataSource;

    @Bean
    JdbcTokenRepositoryImpl jdbcTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        return jdbcTokenRepository;
    }

    //2. SpringSecurity配置相关信息
    @Override
    public void configure(HttpSecurity http) throws Exception {
        // 释放静态资源,指定拦截规则,指定自定义的认证和退出页面,csrf配置等
        http.authorizeRequests()
                // 指定拦截规则
				...
                // 开启记住我功能
                .rememberMe()
                .key("rememberMeKey") // 默认 key为UUID,我们可以自定义 key
                .tokenValiditySeconds(60) //设置token的过期时间,默认2周
                .tokenRepository(jdbcTokenRepository())
                ;
    }

1.2 测试
前端同上,登录认证通过后,关掉浏览器,再次打开页面,remember-me功能生效了,数据表多了一条记录。
在这里插入图片描述
可以看到,cookie 中 remember-me 的使用用 : 隔开,分成了两部分:

  1. 数据表中的 series字段值。
  2. 数据表中的 token字段值。

2、源码分析

分析过程同上,重点关注 PersistentTokenBasedRememberMeServices类

2.1 表记录生成

查看 PersistentTokenBasedRememberMeServices类的 loginSuccess方法。

    protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
        //获取用户名
        String username = successfulAuthentication.getName();
        this.logger.debug("Creating new persistent login for user " + username);
        //1.生成 series和token
        PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());

        try {
            //2.入库
            this.tokenRepository.createNewToken(persistentToken);
            //3.添加token到浏览器的Cookie中
            this.addCookie(persistentToken, request, response);
        } catch (Exception var7) {
            this.logger.error("Failed to save persistent token ", var7);
        }

    }

逻辑如下:

  1. 构造一个 PersistentRememberMeToken 实例,generateSeriesData 和 generateTokenData 方法分别用来获取 series 和 token,具体的生成过程实际上就是调用 SecureRandom 生成随机数再进行 Base64 编码,不同于 Math.random 或者 java.util.Random 这种伪随机数,SecureRandom 则采用的是类似于密码学的随机数生成规则,其输出结果较难预测,适合在登录这样的场景下使用。
  2. 调用 tokenRepository 实例(我们配置的 JdbcTokenRepositoryImpl)中的 createNewToken 方法,将 PersistentRememberMeToken 存入数据库中。
  3. 最后添加 series 和 token 到浏览器 Cookie中。

在这里插入图片描述

public class JdbcTokenRepositoryImpl extends JdbcDaoSupport implements PersistentTokenRepository {
    public static final String CREATE_TABLE_SQL = "create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)";
    public static final String DEF_TOKEN_BY_SERIES_SQL = "select username,series,token,last_used from persistent_logins where series = ?";
    public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
    public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?";
    public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ?";
    private String tokensBySeriesSql = "select username,series,token,last_used from persistent_logins where series = ?";
    private String insertTokenSql = "insert into persistent_logins (username, series, token, last_used) values(?,?,?,?)";
    private String updateTokenSql = "update persistent_logins set token = ?, last_used = ? where series = ?";
    private String removeUserTokensSql = "delete from persistent_logins where username = ?";
    private boolean createTableOnStartup;
    ...

2.2 token解析

查看 rememberMeServices.autoLogin重点方法进行自动登录逻辑。
在这里插入图片描述
查看 PersistentTokenBasedRememberMeServices类的 processAutoLoginCookie方法。

在这里插入图片描述
逻辑如下:

  1. 从前端传来的 cookie 中解析出 series 和 token。
  2. 根据 series 从数据库中查询出一个 PersistentRememberMeToken 实例。
  3. 如果查出来的 token 和前端传来的 token 不相同,此时根据用户名移除相关的 token,相当于必须要重新输入用户名密码登录才能获取新的自动登录权限。
  4. 校验 token 是否过期。
  5. 构造新的 PersistentRememberMeToken 对象,并且更新数据库中的 token信息。
  6. 将新的令牌重新添加到 cookie 中返回。
  7. 根据用户名查询用户信息,再走一波登录流程。

– 求知若饥,虚心若愚。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值