核心原则
-
Access Token:短期访问凭证(10-30分钟),用于业务请求
-
Refresh Token:长期刷新凭证(7-30天),用于获取新 Access Token
- 安全层面:Access Token泄露影响时间短
- 体验层面:无感刷新避免频繁登录
- 控制层面:Refresh Token可单独吊销
双 Token 工作流程
关键设计细节
1. Token 结构
Access Token (JWT):
存储用户详情及各系统权限等
{
"sub": "user123",
"exp": 1624290000, // 30分钟后过期
"scope": "api.read",
"jti": "a1b2c3d4" // 唯一标识
}
Refresh Token (不透明令牌):
存储基本信息,去除敏感信息,仅用于Access Token刷新
// Redis存储结构
key: refresh:user123:client_mobile
value: {
"token": "x9y8z7w6",
"expire_at": 1626872400, // 7天后过期
"client_info": "iPhone13;IOS15"
}
2. 刷新机制
刷新时机:
- 高风险操作后(如修改密码等)
- 跨设备登录
- 定期更新(7day等)
// SSO Server刷新接口
@PostMapping("/refresh")
public TokenResponse refreshTokens(@RequestParam String refreshToken) {
// 1. 验证Refresh Token有效性
RefreshToken stored = redisTemplate.opsForValue().get(refreshToken);
if(stored == null || stored.isRevoked()) {
throw new InvalidTokenException();
}
// 2. 生成新Access Token
String newAccessToken = JwtUtil.generateToken(stored.getUserId());
// 3. 可选:刷新令牌轮转
String newRefreshToken = null;
if(shouldRotateRefreshToken(stored)) {
newRefreshToken = generateRefreshToken(stored.getUserId());
redisTemplate.delete(refreshToken); // 使旧Refresh失效
}
return new TokenResponse(newAccessToken, newRefreshToken);
}
3. 安全防护
风险 | 防护方案 |
---|---|
Access Token泄露 | 短有效期(≤30min) + 黑名单 |
Refresh Token窃取 | 绑定设备/IP + 单次使用轮转 |
重放攻击 | JWT jti唯一标识 + 一次性验证 |
CSRF攻击 | Refresh Token存HttpOnly Cookie |
最佳实践
1. 存储策略
2. 注销协同方案
// 全局登出服务
public void globalLogout(String userId) {
// 1. 清除所有Access Token
redisTemplate.deletePattern("access:*:" + userId);
// 2. 使Refresh Token失效
Set<String> refreshKeys = redisTemplate.keys("refresh:*:" + userId);
refreshKeys.forEach(key -> redisTemplate.delete(key));
// 3. 发布MQ注销事件
mqTemplate.send("user_logout", userId);
}
3. 跨域安全方案
# Nginx配置示例
add_header 'Access-Control-Allow-Origin' 'https://yourdomain.com';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Expose-Headers' 'Authorization';