(八)Remember-Me实现原理:持久化令牌与安全存储方案
最新Spring Security实战教程(八):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中:
1
| 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| @Configuration public class RememberSecurityConfig { @Bean public UserDetailsService users() { UserDetails user = User.withUsername("user") .password("{noop}user") .roles("USER") .build(); UserDetails admin = User.withUsername("admin") .password("{noop}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) .userDetailsService(users()) ); return http.build(); } }
|
代码解析:
rememberMe.key("mySecretKey"):服务器端密钥,防止Token被伪造。
tokenValiditySeconds(7 * 24 * 60 * 60):Token 7天有效。
userDetailsService(users()):Remember-Me认证时使用的UserDetailsService。
配置测试Controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| @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重放攻击。
❶ 持久化令牌方案的工作流程
- 用户登录后,系统生成一个seriesId和tokenValue,并存储到数据库。
- 服务器将seriesId存入Cookie,tokenValue仅存储在数据库。
- 下次用户访问时:
- 服务器从Cookie获取seriesId;
- 从数据库查找对应的tokenValue;
- 验证成功后,生成新的tokenValue并更新数据库(防止Token被重放攻击)。
❷ 数据库表设计 + SpringBoot配置
1 2 3 4 5 6
| 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文件配置博主就复用之前章节的配置内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.9</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-jsqlparser</artifactId> <version>3.5.9</version> </dependency> </dependencies>
|
yml配置文件,只需要追加datasource数据源配置即可:
1 2 3 4 5 6 7 8 9 10 11 12 13
| server: port: 8086
spring: application: name: remember-db-spring-security 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| @Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { private final DataSource dataSource; @Bean public UserDetailsService users() { UserDetails user = User.withUsername("user") .password("{noop}user") .roles("USER") .build(); UserDetails admin = User.withUsername("admin") .password("{noop}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) .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. 持久化令牌方案的安全增强实现
上述讲解中使用官方默认的配置其实已经能满足我们大部分需求,但是我们也会遇到需要自定义令牌生成、验证的情况,这里博主就简单编写两个供大家参考。
令牌生成策略优化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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() ) ); } }
|
令牌验证逻辑强化
1 2 3 4 5 6 7
| @Component public class TokenValidationService { public boolean validateToken(PersistentRememberMeToken token, String presentedToken) { return BCrypt.checkpw(presentedToken, token.getTokenValue()); } }
|
自动清理过期令牌
使用定时器定期清理过期的令牌:
1 2 3 4 5 6 7
| @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)) ); }
|
实时吊销机制
1 2 3 4
| @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的其他高级特性,敬请期待!