一、Token续期的本质
Token续期不是简单的时间重置,而是安全、用户体验和系统性能的三方博弈。
我们先看一个典型事故:
// 错误案例:简单过期的Token检查
public boolean validateToken(String token) {return JwtUtil.getExpiration(token).after(new Date());
}
这种实现会导致:
- 用户操作中断(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
}
三大致命缺陷:
- 旧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;
}
适用场景:
- 内部低安全系统
- 短期活动页面
- 快速原型开发
三、双Token方案
3.1 核心架构设计
3.2 安全增强:三验证机制
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);
}
3.3 并发控制:分布式锁方案
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();}
}
适用场景:
- 金融系统
- 电商平台
- 企业级应用
四、自动续期方案
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);
}
4.2 Redis缓存续期方案
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);}
}
4.3 Gateway全局过滤器方案
@Component
@Order(-100)
publicclass TokenRenewFilter implements GlobalFilter {@Overridepublic 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);}
}
适用场景:
- 微服务架构
- 前后端分离应用
- 高并发用户系统
五、分布式环境特殊挑战
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);
}
5.2 跨服务令牌验证
public boolean validateTokenAcrossServices(String token) {// 1. 本地快速验证if (JwtUtil.verifyWithLocalKey(token)) return true;// 2. 查询认证中心return authCenterClient.validateToken(token);
}
六、五大方案对比
方案 | 安全性 | 用户体验 | 实现复杂度 | 适用场景 | 性能影响 | 典型应用 |
单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("需要短信验证");}}// 执行操作
}
8.2 性能优化关键
- 异步刷新队列:
- 本地缓存验证:
// 使用Caffeine实现本地缓存
LoadingCache<String, Boolean> tokenCache = Caffeine.newBuilder().maximumSize(10_000).expireAfterWrite(5, TimeUnit.MINUTES).build(key -> redis.exists("valid_token:" + key));
8.3 十大避坑指南
- 永远不要用长有效期的Access Token
- Refresh Token必须是一次性使用的
- 客户端必须实现静默更新机制
- 分布式环境下必须用RedLock处理并发刷新
- 敏感操作必须二次认证
- 黑名单有效期需长于Token有效期
- 设备变更必须重新认证
- 监控Refresh Token的使用频率
- 定期轮换签名密钥
- 实现令牌撤销接口