SpringSecurity实战教程(八)Remember-Me实现原理:持久化令牌与安全存储方案
1. 前言
在我们日常开发的后台系统中,“Remember-Me”(记住我)功能是一种常见的安全增强机制,允许用户在关闭浏览器后仍然保持登录状态,而无需重新输入用户名和密码。Spring Security提供了多种Remember-Me方案,最常用的是基于哈希的Token方案和持久化令牌方案。
本章节博主将详细讲解这两种方案的实现,带大家快速入门!在实际开发中,小伙伴们可根据各自需求进行改造。
2. Remember-Me机制概述
在Spring Security中,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:
@Configurationpublic 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:
@Controllerpublic 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重放攻击。
❶ 持久化令牌方案的工作流程
- 用户登录后,系统生成一个seriesId和tokenValue,并存储到数据库。
- 服务器将seriesId存入Cookie,tokenValue仅存储在数据库。
- 下次用户访问时:
- 服务器从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@RequiredArgsConstructorpublic 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数据:

同时关闭浏览器重新访问/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() ) ); }}令牌验证逻辑强化
@Componentpublic 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+UA | 在PersistentToken中存储用户特征,验证时比对 |
| 暴力破解 | 增加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 Security中Remember-Me机制的深入解析,包含Token方案和持久化令牌方案的完整实现及源码讲解。通过本章节的学习,相信大家已经能够根据实际业务需求选择合适的Remember-Me方案,并在需要时进行安全增强定制。
在后续的章节中,我们将继续深入探讨Spring Security的其他高级特性,敬请期待!
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!
樊笼










