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

news/2024/7/4 8:30:00

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、源码分析

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

  • Spring Security登录认证源码分析:https://blog.csdn.net/qq_42402854/article/details/122295175

查看 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. 根据用户名查询用户信息,再走一波登录流程。

– 求知若饥,虚心若愚。


http://www.niftyadmin.cn/n/2257456.html

相关文章

榆熙电商:为何购物旗舰店选择优于其他?

用户们在购买某个店铺的产品的时候&#xff0c;往往都是优先选择在旗舰店进行购买&#xff0c;那么该如何去分辨拼多多的旗舰店呢&#xff1f;旗舰店有哪些保障呢&#xff1f;一起和成都榆熙小编来了解一下吧。 一、用户如何识别官方旗舰店&#xff1f; 最简单的方法就是看店…

Spring Security CSRF防御源码分析

一、CSRF简介 1、CSRF是什么&#xff1f; CSRF&#xff08;Cross-site request forgery&#xff09;&#xff0c;也被称为&#xff1a;one click attack/session riding&#xff0c;中文名称&#xff1a;跨站请求伪造&#xff0c;通常缩写为&#xff1a;CSRF/XSRF。 跨站请求…

榆熙电商:拼多多商家如何优化用户春节期间的购物体验?

春节将至&#xff0c;在拼多多开店的商家很可能会面临合作物流方春节期间不发货的情况。拼多多商家面对这样的情况又该如何处理呢&#xff1f;今天榆熙电商小编就跟大家聊一聊&#xff0c;在拼多多开店&#xff0c;春节期间合作物流不发货怎么办&#xff1f; 一、拼多多春节放…

Spring Security整合Spring Boot分布式认证

一、分布式系统认证技术分析 1、分布式系统认证 随着软件环境和需求的变化&#xff0c;软件的架构通常都会由单体结构演变成具有分布式架构的分布式系统。而分布式系统的每个服务都会有认证、授权的需求。如果每个服务都实现一套认证逻辑&#xff0c;就会非常冗余并且不现实。…

自定义异常及异常全局处理

目录【Java异常分类】【案例分析】【测试】【总结】【Java异常分类】 java中异常均继承自Throwable,其有两个重要的直接子类error与exception Error&#xff1a; 大部分是由虚拟机报出来的错误&#xff0c;是程序无法处理的错误,如 OutOfMemoryError,当JVM需要更多内存空间而…

榆熙电商:小编推荐的产品会得到什么好处?

相信有的用户在拼多多浏览产品的时候&#xff0c;有的产品带有一个小编推荐的标识&#xff0c;那么这个小编推荐是个啥呢&#xff1f;带有这个标签的产品可靠吗&#xff1f;商家又该如何获得小编推荐呢&#xff1f;带着这些疑惑一起和榆熙电商小编来看看吧。 一、小编推荐是什…

成都榆熙电子商务有限公司:拼多多商家如何累计流量扶持?

每个想在拼多多尝试创业的朋友都可以轻松把店开起来&#xff0c;然而如何经营却各有千秋。有的人能够让店铺占据强劲的竞争优势地位&#xff0c;有的人能够保持店铺流量处于稳定活跃的状态&#xff0c;也有在拼多多开店没有流量的。大家的关键差异在于&#xff0c;打造优质拼多…

12 种 Spring 常用注解!

更多注解详情&#xff1a; https://mp.weixin.qq.com/s/O6mm0N9VcK9FQhC6Ole-eA 1.声明bean的注解 // 组件&#xff0c;没有明确的角色 Component // 在展现层使用&#xff0c;控制器的声明&#xff08;controller 层&#xff09; Controller // 在业务逻辑层使用&#xff0…