最新Spring Security实战教程(八)Remember-Me实现原理 - 持久化令牌与安全存储方案

在这里插入图片描述

🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志
🎐 个人CSND主页——Micro麦可乐的博客
🐥《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程,入门到实战
🌺《RabbitMQ》专栏19年编写主要介绍使用JAVA开发RabbitMQ的系列教程,从基础知识到项目实战
🌸《设计模式》专栏以实际的生活场景为案例进行讲解,让大家对设计模式有一个更清晰的理解
🌛《开源项目》本专栏主要介绍目前热门的开源项目,带大家快速了解并轻松上手使用
✨《开发技巧》本专栏包含了各种系统的设计原理以及注意事项,并分享一些日常开发的功能小技巧
💕《Jenkins实战》专栏主要介绍Jenkins+Docker的实战教程,让你快速掌握项目CI/CD,是2024年最新的实战教程
🌞《Spring Boot》专栏主要介绍我们日常工作项目中经常应用到的功能以及技巧,代码样例完整
🌞《Spring Security》专栏中我们将逐步深入Spring Security的各个技术细节,带你从入门到精通,全面掌握这一安全技术
如果文章能够给大家带来一定的帮助!欢迎关注、评论互动~

回顾链接:
最新Spring Security实战教程(一)初识Spring Security安全框架
最新Spring Security实战教程(二)表单登录定制到处理逻辑的深度改造
最新Spring Security实战教程(三)Spring Security 的底层原理解析
最新Spring Security实战教程(四)基于内存的用户认证
最新Spring Security实战教程(五)基于数据库的动态用户认证传统RBAC角色模型实战开发
最新Spring Security实战教程(六)最新Spring Security实战教程(六)基于数据库的ABAC属性权限模型实战开发
最新Spring Security实战教程(七)方法级安全控制@PreAuthorize注解的灵活运用

专栏更新完毕后,博主将会上传所有章节代码到优快云资源免费给大家下载,如你不想等后续章节代码需提前获取,可以私信或留言!

1. 前言

在我们日常开发中登陆后台,“Remember-Me”(记住我)功能是一种常见的安全增强机制,允许用户在关闭浏览器后仍然保持登录状态,而无需重新输入用户名和密码。Spring Security 提供了多种 Remember-Me 方案,最常用的是基于哈希的Token方案持久化令牌方案

本章节博主将详细讲解这两种方案的实现,带大家快速入门!在小伙伴们实际开发中可进行各自需求的改造!


2. Remember-Me 机制概述

Spring Security 中,Remember-Me 的核心作用是在会话失效后依然允许用户自动登录。其基本工作流程(以更常用的持久化令牌方案为例)如下:
在这里插入图片描述

  • 用户登录成功后,如果勾选了“记住我”,服务器会创建一个 Remember-Me Token,并存储在客户端的 Cookie
  • 当用户的会话失效后,系统会检查 Cookie 是否存在并有效:
    1、如果有效,则自动完成登录;
    2、如果无效或过期,则用户需要重新认证。

Spring Security 主要提供两种 Remember-Me 方案:

基于 Token(默认方案):在 Cookie 中存储加密 Token默认使用 TokenBasedRememberMeServices

持久化令牌方案(更安全):将 Token 存储在数据库中,并在每次 Remember-Me 认证时进行更新(使用 PersistentTokenBasedRememberMeServices


3. 基于 Token 的 Remember-Me 机制

Spring Security 默认提供 TokenBasedRememberMeServices,其基本原理如下:

  • 当用户登录时,系统生成一个 Token,并将其存储在 Cookie 中:
Base64(username + ":" + expirationTime + ":" + MD5(username + ":" + expirationTime + ":" + password + ":" + key))
  • 后续每次请求时,系统从 Cookie 读取 Token,并验证其正确性:
  • 检查 Token 是否未过期;
  • 重新计算 MD5 哈希值,并与 Token 中的值进行对比;
  • 验证通过后自动完成登录。

开始配置 Token 方案

为了快速演示这里我们就用我们第三章节中基于内存的用户认证的模块代码来追加演示,复用 maven项目中 memory-spring-security 子模块代码,新建一个 remember-spring-security 子模块
如小伙伴没了解基于内存的用户认证的相关知识可以访问 最新Spring Security实战教程(三)Spring Security 的底层原理解析 进行学习!

配置 Spring Security

@Configuration
public class RememberSecurityConfig {

    // 手动配置用户信息
    @Bean
    public UserDetailsService users() {
        UserDetails user = User.withUsername("user")
                .password("{noop}user") // {noop}表示不加密
                .roles("USER")
                .build();

        UserDetails admin = User.withUsername("admin")
                .password("{noop}admin") // {noop}表示不加密
//                .password(passwordEncoder().encode("admin"))
                .roles("ADMIN")
                .build();

        UserDetails anonymous = User.withUsername("anonymous")
                .password("{noop}anonymous")
                .roles("ANONYMOUS")
                .build();

        return new InMemoryUserDetailsManager(user, admin, anonymous);
    }

    // 配置安全策略
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.
                authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/admin/**").hasRole("ADMIN")
                        .anyRequest().authenticated()
                )
                .formLogin(withDefaults())
                .logout(withDefaults())
                .rememberMe(rememberMe -> rememberMe
                        .key("mySecretKey") // 服务器端密钥
                        .tokenValiditySeconds(7 * 24 * 60 * 60) // 7天有效期
                        .userDetailsService(users())) // 认证用户信息
        ;
        return http.build();
    }

}

代码解析
rememberMe.key(“mySecretKey”) :服务器端密钥,防止 Token 被伪造。
tokenValiditySeconds(7 * 24 * 60 * 60) :Token 7 天有效。
userDetailsService(users()) :Remember-Me 认证时使用的 UserDetailsService。

配置 测试controller

@Controller
public class DemoRememberController {

    @GetMapping("/")
    public ResponseEntity<Map<String, Object>> index(Authentication authentication) {

        String username = authentication.getName();//用户名
        Object principal =authentication.getPrincipal();//身份
        // 获取用户拥有的权限列表
        List<String> roles = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.toList());
        //返回用户信息
        return ResponseEntity.ok(Map.of(
                "username", username,
                "principal", principal,
                "roles", roles));
    }

    @GetMapping("/admin/view")
    public ResponseEntity<String> admin() {
        return ResponseEntity.ok("管理员ADMIN角色访问ok");
    }
}

访问 /login 并输入管理员用户名/密码(admin),勾选 “Remember-Me” 选项。
登录成功后,查看浏览器 Cookie:
在这里插入图片描述
关闭浏览器后重新访问 /admin/view,系统会自动完成认证,无需重新输入用户名/密码。

安全提示:

使用Base64简单加密,令牌无状态、易预测
由于 Token 直接存储在 Cookie 中,一旦被盗,攻击者可直接伪造登录
仅适用于低安全性要求的系统,不推荐在金融、政府、企业级应用中使用


4. 持久化令牌方案

持久化令牌方案相比 Token 方案更安全:

  • Token 存储在数据库,而非 Cookie,避免被轻易伪造。
  • 每次 Remember-Me 认证时,生成新的 Token 并存入数据库,防止 Token 重放攻击。

❶ 持久化令牌方案的工作流程

  1. 用户登录后,系统生成一个 seriesId 和 tokenValue,并存储到数据库;
  2. 服务器将 seriesId 存入 Cookie,tokenValue 仅存储在数据库;
  3. 下次用户访问时:

服务器从 Cookie 获取 seriesId;
从数据库查找对应的 tokenValue;
验证成功后,生成新的 tokenValue 并更新数据库(防止 Token 被重放攻击)

❷ 数据库表设计 + sprigboot配置

CREATE TABLE persistent_logins (
    username VARCHAR(64) NOT NULL,
    series VARCHAR(64) PRIMARY KEY,
    token VARCHAR(64) NOT NULL,
    last_used TIMESTAMP NOT NULL
);

因为涉及使用数据源,这里 pom 文件配置博主就复用之前章节的配置内容:

    <dependencies>
        <!--使用 HikariCP 连接池-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!-- mysql驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.30</version>
        </dependency>
        <!-- mybatis-plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
            <version>3.5.9</version>
        </dependency>
        <!-- jdk 11+ 引入可选模块 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-jsqlparser</artifactId>
            <version>3.5.9</version>
        </dependency>
    </dependencies>

yml 配置文件,只需要追加datasource数据源配置即可

server:
  port: 8086

spring:
  application:
    name: remember-db-spring-security #最新Spring Security实战教程(八)Remember-Me实现原理 - 持久化令牌与安全存储方案
  datasource:
    url: jdbc:mysql://localhost:3306/slave_db?useSSL=false&serverTimezone=UTC
    username: root
    password: toher888
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 5

❸ Spring Security 配置

这里博主就不演示从数据库获取用户信息认证了,这里就使用手动配置用户信息。
需要了解的小伙伴可以访问 最新Spring Security实战教程(五)基于数据库的动态用户认证传统RBAC角色模型实战开发 进行学习了解

注入数据源,添加 PersistentTokenRepository

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

	//注入数据源
    private final DataSource dataSource;

    // 手动配置用户信息
    @Bean
    public UserDetailsService users() {
        UserDetails user = User.withUsername("user")
                .password("{noop}user") // {noop}表示不加密
                .roles("USER")
                .build();

        UserDetails admin = User.withUsername("admin")
                .password("{noop}admin") // {noop}表示不加密
//                .password(passwordEncoder().encode("admin"))
                .roles("ADMIN")
                .build();

        UserDetails anonymous = User.withUsername("anonymous")
                .password("{noop}anonymous")
                .roles("ANONYMOUS")
                .build();

        return new InMemoryUserDetailsManager(user, admin, anonymous);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/admin").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(withDefaults())
            .rememberMe(rememberMe -> rememberMe
                .key("myPersistentKey")
                .tokenRepository(persistentTokenRepository()) // 采用数据库存储
                .tokenValiditySeconds(14 * 24 * 60 * 60) // 14 天有效期
                .userDetailsService(userDetailsService())
            );
        return http.build();
    }

    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
        repo.setDataSource(dataSource);
        return repo;
    }
}

1、tokenRepository(persistentTokenRepository()):使用数据库存储 Token;
2、JdbcTokenRepositoryImpl:Spring 提供的默认数据库 Token 存储实现;
3、tokenValiditySeconds(14 * 24 * 60 * 60):Token 有效期设定为 14 天。

启动运行测试进行登陆,会发现数据库中多了一条Token数据,同时关闭浏览器重新访问 /admin/view,系统会自动完成认证,无需重新输入用户名/密码。

在这里插入图片描述

5. 持久化令牌方案的安全增强实现

上述讲解中使用官方默认的配置其实已经能满足我们大部分需求,但是我们也会遇到需要自定义令牌生成、验证,这里博主就简单编写两个供大家参考

令牌生成策略优化

public class CustomPersistentTokenRepository extends JdbcTokenRepositoryImpl {
    
    @Override
    public void createNewToken(PersistentRememberMeToken token) {
        String encryptedToken = BCrypt.hashpw(token.getTokenValue(), BCrypt.gensalt());
        super.createNewToken(
            new PersistentRememberMeToken(
                token.getUsername(),
                token.getSeries(),
                encryptedToken,
                token.getDate()
            )
        );
    }
}

令牌验证逻辑强化

@Component
public class TokenValidationService {

    public boolean validateToken(PersistentRememberMeToken token, String presentedToken) {
        return BCrypt.checkpw(presentedToken, token.getTokenValue());
    }
}

自动清理过期令牌

使用定时器定期清理过期的令牌

@Scheduled(fixedRate = 24 * 60 * 60 * 1000) // 每日清理
public void purgeExpiredTokens() {
    jdbcTemplate.update(
        "DELETE FROM persistent_logins WHERE last_used < ?",
        Date.from(Instant.now().minus(60, ChronoUnit.DAYS))
    );
}

实时吊销机制

@PostMapping("/revoke-remember-me")
public void revokeTokens(@AuthenticationPrincipal User user) {
    tokenRepository.removeUserTokens(user.getUsername());
}

多维度安全防护

攻击类型防护措施实现方法
令牌窃取令牌绑定IP+UAPersistentToken中存储用户特征,验证时比对
暴力破解增加BCrypt计算成本BCrypt.hashpw(token, BCrypt.gensalt(12))
重放攻击单次使用令牌每次验证后更新令牌
CSRF启用SameSite Cookie.rememberMe().cookie().samesite(SameSite.STRICT)

最佳实践

生产环境建议使用 持久化令牌方案,避免 Token 被伪造。
Token 存储应使用 加密存储(如 BCrypt)。
对 persistent_logins 表定期清理,避免 Token 长期滞留。


结语

至此我们就完成了 Spring Security Remember-Me 机制 的深入解析,包含 Token方案持久化令牌方案 的完整实现及源码讲解。

如果本本章内容对您有所帮助,希望 一键三连 给博主一点点鼓励,如果您有任何疑问或建议,请随时留言讨论!


下一章:最新Spring Security实战教程(九)前后端分离认证实战 - JWT+SpringSecurity无缝整合

在这里插入图片描述

评论 38
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Micro麦可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值