Token續期的五種方案
今天我們來聊聊一個看似簡單卻讓無數開發者栽跟頭的問題——Token續期。
你以為Token續期只是重置時間?90%的系統安全漏洞由此而生!
當用戶正在提交重要表單時突然跳轉到登錄頁面,或者系統在高峰期因Token并發刷新而崩潰,這些問題的根源往往在于Token續期策略設計不當。
一、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 {
@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);
}
}適用場景:
- 微服務架構
- 前后端分離應用
- 高并發用戶系統
五、分布式環境特殊挑戰
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的使用頻率
- 定期輪換簽名密鑰
- 實現令牌撤銷接口
好的Token管理系統應該像人體的自主神經系統——平時感受不到它的存在,但在需要時總能及時響應。

































