Token续期的五种方案
今天我们来聊聊一个看似简单却让无数开发者栽跟头的问题——Token续期。
你以为Token续期只是重置时间?90%的系统安全漏洞由此而生!
当用户正在提交重要表单时突然跳转到登录页面,或者系统在高峰期因Token并发刷新而崩溃,这些问题的根源往往在于Token续期策略设计不当。
一、Token续期的本质
Token续期不是简单的时间重置,而是安全、用户体验和系统性能的三方博弈。
我们先看一个典型事故:
复制
// 错误案例:简单过期的Token检查
public boolean validateToken(String token) {
return JwtUtil.getExpiration(token).after(new Date());
}1.2.3.4.
这种实现会导致:
用户操作中断(Token突然过期)安全风险(旧Token继续有效)并发问题(多个请求同时触发刷新)Token续期的三大核心问题何时续期:提前多久刷新最合理?如何续期:单Token还是双Token?有状态还是无状态?安全防控:如何防止令牌劫持和并发风暴?下面我跟大家一起聊聊工作中最常用的5种主流方案,希望对你会有所帮助。
二、单Token方案
2.1 基础实现与致命缺陷复制
public String refreshToken(String oldToken) {
String username = JwtUtil.parseUsername(oldToken);
return JwtUtil.generateToken(username, 30 * 60); // 直接生成新Token
}1.2.3.4.
三大致命缺陷:
旧Token在有效期内依然可用(安全黑洞)多个请求同时触发刷新会导致多个有效Token并存(并发灾难)无法强制下线用户(状态失控)2.2 黑名单优化方案图片
代码实现:
复制
public String safeRefresh(String oldToken) {
// 旧Token加入黑名单(有效期比Token长5分钟)
redis.setex("blacklist:"+oldToken, "1", 35 * 60);
String username = JwtUtil.parseUsername(oldToken);
String newToken = JwtUtil.generateToken(username, 30 * 60);
return newToken;
}1.2.3.4.5.6.7.8.
适用场景:
内部低安全系统短期活动页面快速原型开发三、双Token方案
3.1 核心架构设计图片
复制
public TokenPair refreshTokens(String refreshToken) {
// 1. JWT签名验证
if (!JwtUtil.verifySignature(refreshToken)) {
thrownew SecurityException("非法令牌");
}
// 2. 状态令牌验证
String stateToken = extractStateToken(refreshToken);
if (!redis.exists("state_token:" + stateToken)) {
thrownew SecurityException("令牌已失效");
}
// 3. 设备绑定验证
String deviceId = getDeviceIdFromRequest();
if (!deviceId.equals(redis.get("bind_device:" + stateToken))) {
thrownew SecurityException("设备变更需重新登录");
}
return generateNewTokenPair(refreshToken);
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.
复制
public TokenPair safeRefresh(String refreshToken) {
String lockKey = "refresh_lock:" + refreshToken;
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(2, 5, TimeUnit.SECONDS)) {
return doRefresh(refreshToken);
}
throw new BusyException("系统繁忙,请重试");
} finally {
lock.unlock();
}
}1.2.3.4.5.6.7.8.9.10.11.12.13.
适用场景:
金融系统电商平台企业级应用四、自动续期方案
4.1 拦截器+滑动窗口图片
智能阈值计算:
复制
public boolean shouldRenew(Token token) {
long remainTime = token.getExpireTime() - System.currentTimeMillis();
long totalTime = token.getTotalValidTime();
// 双阈值策略:绝对时间(5分钟)和相对时间(30%有效期)
return remainTime <= Math.min(5 * 60 * 1000, 0.3 * totalTime);
}1.2.3.4.5.6.7.
复制
public void autoRenewToken(String headerToken) {
String cacheKey = "token_cache:" + headerToken;
String cacheToken = redis.get(cacheKey);
if (cacheToken == null) throw new TokenExpiredException("令牌已完全过期");
if (JwtUtil.isAboutToExpire(cacheToken)) {
String newToken = generateNewToken();
// 关键:Token更新但缓存Key不变
redis.setex(cacheKey, newToken, 2 * 60 * 60);
response.setHeader("X-New-Token", newToken);
}
}1.2.3.4.5.6.7.8.9.10.11.12.13.
复制
@Component
@Order(-100)
publicclass TokenRenewFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, Chain chain) {
String token = extractToken(exchange.getRequest());
if (renewService.isRenewRequired(token)) {
String newToken = renewService.renewToken(token);
exchange.getResponse().getHeaders().set("X-New-Token", newToken);
}
return chain.filter(exchange);
}
}1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.
适用场景:
微服务架构前后端分离应用高并发用户系统五、分布式环境特殊挑战
5.1 多设备会话管理图片
设备冲突解决方案:
复制
public void handleLogin(User user, String deviceType) {
String oldSessionKey = "user_devices:" + user.getId() + ":" + deviceType;
String oldToken = redis.get(oldSessionKey);
if (oldToken != null) {
redis.del("token_cache:" + oldToken); // 使旧Token失效
}
String newToken = generateToken();
redis.set(oldSessionKey, newToken);
}1.2.3.4.5.6.7.8.9.10.11.
复制
public boolean validateTokenAcrossServices(String token) {
// 1. 本地快速验证
if (JwtUtil.verifyWithLocalKey(token)) return true;
// 2. 查询认证中心
return authCenterClient.validateToken(token);
}1.2.3.4.5.6.7.
六、五大方案对比
方案
安全性
用户体验
实现复杂度
适用场景
性能影响
典型应用
单Token基础版
★☆☆☆☆
★★☆☆☆
★☆☆☆☆
内部测试系统
低
原型开发
单Token+黑名单
★★☆☆☆
★★★☆☆
★★☆☆☆
低风险Web应用
中
企业内网
双Token基础版
★★★☆☆
★★★★☆
★★★☆☆
常规Web/APP
中
电商平台
双Token+三验证
★★★★★
★★★☆☆
★★★★☆
金融/支付系统
高
银行APP
自动续期方案
★★★★☆
★★★★★
★★★★☆
高用户体验要求系统
中高
SAAS应用
七、方案如何选型?
图片
八、最佳实践与避坑指南
8.1 安全黄金法则
令牌时效控制:Access Token ≤ 30分钟
Refresh Token ≤ 7天(需配合刷新次数限制)
敏感操作二次认证:复制
public void processSensitiveOperation(String token) {
if (isSensitiveOperation()) {
if (!smsVerifyService.verify(getCurrentUser())) {
throw new SecurityException("需要短信验证");
}
}
// 执行操作
}1.2.3.4.5.6.7.8.
8.2 性能优化关键
异步刷新队列:图片
复制
// 使用Caffeine实现本地缓存
LoadingCache<String, Boolean> tokenCache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(key -> redis.exists("valid_token:" + key));1.2.3.4.5.
8.3 十大避坑指南
永远不要用长有效期的Access TokenRefresh Token必须是一次性使用的客户端必须实现静默更新机制分布式环境下必须用RedLock处理并发刷新敏感操作必须二次认证黑名单有效期需长于Token有效期设备变更必须重新认证监控Refresh Token的使用频率定期轮换签名密钥实现令牌撤销接口好的Token管理系统应该像人体的自主神经系统——平时感受不到它的存在,但在需要时总能及时响应。
THE END