前言
在Web应用开发中,Token管理是一个绕不开的话题。如何在不影响用户体验的前提下保证安全性?这是很多开发者面临的难题。
单Token方案存在一个经典的两难困境:
- 有效期短:安全性高,但用户频繁登录,体验极差
- 有效期长:用户省心,但Token一旦泄露,风险显著增加
有没有办法既保证安全,又让用户无感知?答案是------双Token机制。
本文将系统讲解OAuth2双Token机制的核心原理,并通过Spring Security OAuth2实现完整的双Token方案,同时深入分析配置过程中常见的坑点与解决方案。
一、双Token机制概述
1.1 什么是双Token机制?
双Token机制通过引入两种不同生命周期的Token,将"鉴权"和"续期"两个职责分离:
| Token类型 | 用途 | 有效期 | 特点 |
|---|---|---|---|
| access_token | 接口请求鉴权 | 短期(如24小时) | 频繁使用,泄露影响面小 |
| refresh_token | 刷新access_token | 长期(如30天) | 低频使用,存储在安全位置 |
1.2 为什么需要双Token?
双Token机制解决了单Token方案的三个核心痛点:
痛点一:安全性与用户体验的矛盾
单Token无法同时满足:短期安全但体验差,长期体验好但风险高。双Token将两者解耦,access_token短期保安全,refresh_token长期保体验。
痛点二:Token泄露风险
长期Token一旦泄露,可被长期利用。双Token方案中,access_token短期失效,refresh_token可随时吊销。
痛点三:无法无感知续期
用户正在操作时Token突然过期,可能导致数据丢失。双Token支持静默刷新,用户完全无感知。
1.3 核心工作流程
用户登录成功后,服务端返回两个Token:
- access_token:用于后续API请求的鉴权,有效期较短
- refresh_token:用于在access_token过期后获取新的Token对,有效期较长
当access_token过期时,前端自动调用刷新接口,携带refresh_token换取新的Token对。整个过程中用户无感知,业务操作不中断。
当refresh_token也过期时,用户需要重新登录。
1.4 refresh_token的滚动过期机制
refresh_token的有效期采用滚动过期机制:
- 首次登录:生成refresh_token,有效期为30天
- 每次刷新:生成新的refresh_token,有效期顺延,旧token立即失效
- 持续使用的用户:Token可以无限续期,永不掉线
- 停用超过30天的用户:refresh_token自动过期,需重新登录
这种机制在安全性和用户体验之间取得了良好的平衡。
二、Spring Security OAuth2 核心配置
2.1 核心配置项:reuseRefreshTokens
在Spring Security OAuth2中,reuseRefreshTokens 是控制刷新行为的核心配置:
| 配置值 | 行为 | 安全性分析 | 推荐 |
|---|---|---|---|
| true | 刷新时复用旧的refresh_token | 较低:同一token可被多次使用 | 不推荐 |
| false | 每次刷新生成新的refresh_token | 较高:旧token立即失效,支持token吊销追踪 | 推荐 |
配置示例:
java
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.reuseRefreshTokens(false)
// ... 其他配置
}
2.2 授权服务器配置
java
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private DataSource dataSource;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.userDetailsService(userDetailsService)
.tokenStore(tokenStore())
.authenticationManager(authenticationManager)
.authorizationCodeServices(new JdbcAuthorizationCodeServices(dataSource))
.reuseRefreshTokens(false) // 每次刷新生成新refresh_token
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}
@Bean
public TokenStore tokenStore() {
// 可根据实际需求选择 RedisTokenStore 或 JdbcTokenStore
return new RedisTokenStore(redisConnectionFactory());
}
}
2.3 客户端配置
客户端信息建议存储在数据库中,便于动态管理:
sql
CREATE TABLE `oauth_client_details` (
`client_id` varchar(256) NOT NULL,
`resource_ids` varchar(256) DEFAULT NULL,
`client_secret` varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`authorized_grant_types` varchar(256) DEFAULT NULL,
`web_server_redirect_uri` varchar(256) DEFAULT NULL,
`authorities` varchar(256) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(256) DEFAULT NULL,
PRIMARY KEY (`client_id`)
);
有效期配置建议:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| access_token_validity | 86400 | 24小时 |
| refresh_token_validity | 2592000 | 30天 |
2.4 刷新接口实现
java
@RestController
public class TokenController {
@Autowired
private RestTemplate restTemplate;
@PostMapping("/token/refresh")
public TokenResponse refreshToken(@RequestBody TokenRefreshRequest request) {
String refreshToken = request.getRefreshToken();
if (refreshToken == null || refreshToken.isEmpty()) {
throw new IllegalArgumentException("refresh_token不能为空");
}
// 构造刷新请求
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.set("grant_type", "refresh_token");
params.set("refresh_token", refreshToken);
params.set("client_id", clientId);
params.set("client_secret", clientSecret);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(params, headers);
try {
ResponseEntity<String> response = restTemplate.postForEntity(
tokenEndpointUrl,
entity,
String.class
);
return parseTokenResponse(response.getBody());
} catch (HttpClientErrorException e) {
if (e.getStatusCode() == HttpStatus.BAD_REQUEST) {
throw new IllegalStateException("refresh_token无效或已过期,请重新登录");
}
throw new RuntimeException("刷新token失败", e);
}
}
}
三、常见配置陷阱与解决方案
3.1 陷阱一:多模块配置冲突
现象描述:
- 授权服务器模块已配置
reuseRefreshTokens(false) - 但刷新接口仍然报错或token被复用
- 配置修改后未按预期生效
原因分析:
在微服务架构中,通常存在多个模块配置Token相关功能:
- 授权服务器模块:负责生成和刷新Token
- 资源服务器模块:负责Token校验
当资源服务器模块的配置标记了 @Primary 注解时,其配置优先级会高于授权服务器模块。如果资源服务器模块未显式设置 reuseRefreshTokens(false)(默认值为true),就会覆盖授权服务器的配置。
Spring Bean加载规则:
- 存在
@Primary标记的Bean时,优先使用 - 没有则按名称匹配
解决方案:
两个模块必须同时配置,保持一致:
java
// 资源服务器模块配置
@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore());
tokenServices.setReuseRefreshToken(false); // 必须显式设置
tokenServices.setSupportRefreshToken(true); // 必须开启刷新支持
return tokenServices;
}
3.2 陷阱二:缓存导致配置不生效
现象描述:
- 已修改配置并重启服务
- 但行为仍然不符合预期
原因分析:
当使用Redis作为Token存储时,OAuth2会在Redis中缓存Token相关的数据。修改配置后,旧的Token数据仍然存在于Redis中,可能导致配置未生效。
解决方案:
修改配置后,清理Redis中的相关缓存:
bash
# 查看相关key
KEYS access:*
KEYS refresh:*
# 删除指定key
DEL access:xxx
DEL refresh:xxx
# 或批量清理(仅限测试环境)
redis-cli KEYS "*oauth*" | xargs redis-cli DEL
注意:生产环境请谨慎操作,建议逐个确认后再删除。
3.3 陷阱三:刷新后旧Token仍可用
现象描述:
- 刷新获取新Token后,旧的refresh_token仍然可以继续刷新
原因分析:
reuseRefreshTokens配置为true
解决方案:
- 确认
reuseRefreshTokens设置为false - 清理Redis缓存
- 重新登录获取新的Token对进行验证
验证方法:
- 登录获取 refresh_token_1
- 调用刷新接口获取 refresh_token_2
- 尝试用 refresh_token_1 再次刷新
- 预期结果:返回错误,提示token无效
四、最佳实践总结
4.1 配置检查清单
| 检查项 | 说明 |
|---|---|
| reuseRefreshTokens(false) | 授权服务器模块已配置 |
| setReuseRefreshToken(false) | 资源服务器模块(如有)已配置 |
| setSupportRefreshToken(true) | 资源服务器模块已开启刷新支持 |
| 有效期合理配置 | access_token 24h,refresh_token 30d |
| 缓存已清理 | 修改配置后清理Redis中的旧数据 |
| 降级处理 | 刷新失败时引导用户重新登录 |
4.2 方案对比
| 维度 | 单Token | 双Token |
|---|---|---|
| 安全性 | 低或用户体验差 | 高 |
| 用户体验 | 差或安全性低 | 好 |
| Token吊销 | 困难 | 支持 |
| 实现复杂度 | 简单 | 中等 |
| 适用场景 | 内部系统、低敏感场景 | 面向用户的生产系统 |
4.3 核心要点
- access_token:短期(24h),用于API鉴权,泄露影响有限
- refresh_token:长期(30d),用于无感知刷新,需安全存储
- reuseRefreshTokens :必须设置为
false,确保每次刷新生成新token - 多模块配置:授权服务器与资源服务器配置必须保持一致
- 缓存清理:修改配置后必须清理Redis中的旧token数据
- 滚动过期:持续使用可无限续期,停用30天后需重新登录
写在最后
双Token机制是OAuth2协议中的重要设计,理解其原理并正确配置,可以有效兼顾安全性和用户体验。
本文从原理到实践,系统讲解了Spring Security OAuth2双Token机制的完整实现,并深入分析了常见的配置陷阱。希望对你有所帮助。
参考资料: