title: (八)Remember-Me实现原理:持久化令牌与安全存储方案
categories: [程技]
tags: [Spring Security]

date: 2026-03-02

1. 前言

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

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


2. Remember-Me机制概述

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

Remember-Me工作流程

  • 用户登录成功后,如果勾选了"记住我",服务器会创建一个Remember-Me Token,并存储在客户端的Cookie中。
  • 当用户的会话失效后,系统会检查Cookie是否存在并有效:

    • 如果有效,则自动完成登录;
    • 如果无效或过期,则用户需要重新认证。

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

方案实现类特点
基于Token(默认方案)TokenBasedRememberMeServices在Cookie中存储加密Token,简单但安全性较低
持久化令牌方案(更安全)PersistentTokenBasedRememberMeServices将Token存储在数据库中,每次认证时更新,更安全

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}表示不加密
                .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");
    }
}

测试步骤:

  1. 访问/login并输入管理员用户名/密码(admin),勾选"Remember-Me"选项。
  2. 登录成功后,查看浏览器Cookie。
  3. 关闭浏览器后重新访问/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被重放攻击)。

❷ 数据库表设计 + SpringBoot配置

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}表示不加密
                .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(users())
            );
        return http.build();
    }
    
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl repo = new JdbcTokenRepositoryImpl();
        repo.setDataSource(dataSource);
        return repo;
    }
}

关键配置说明:

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

启动运行测试进行登录,会发现数据库中多了一条Token数据:

数据库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长期滞留。
  • 结合设备指纹技术,增强Token安全性。

6. 两种方案对比总结

对比维度基于Token方案持久化令牌方案
安全性较低,Token易被伪造较高,数据库存储+动态更新
性能无需数据库查询,性能好需要数据库操作,有一定开销
可扩展性无法实现吊销等高级功能支持吊销、审计等高级功能
适用场景低安全性要求的简单应用金融、政务、企业级应用
实现复杂度简单中等

结语

至此我们就完成了Spring SecurityRemember-Me机制的深入解析,包含Token方案持久化令牌方案的完整实现及源码讲解。通过本章节的学习,相信大家已经能够根据实际业务需求选择合适的Remember-Me方案,并在需要时进行安全增强定制。

在后续的章节中,我们将继续深入探讨Spring Security的其他高级特性,敬请期待!

最后修改:2026 年 03 月 12 日
如果觉得我的文章对你有用,请随意赞赏