JWT(JSON Web Token)广泛应用于现代 Web 开发中的认证与授权,它以无状态、灵活和高效的特点深受开发者欢迎。然而,JWT 的一个核心问题是 Token 过期后如何处理。本文将总结常见的解决方案,分析其优缺点,并帮助开发者选择适合自己项目的方案。
JWT 过期问题的挑战
- 安全性:如果 Token 长时间有效,泄露后可能导致严重的安全问题;短时间有效则需要频繁刷新。
- 无状态性:JWT 通常是无状态的,如何在不依赖后端存储的情况下管理过期 Token 成为难点。
- 刷新机制:如何设计高效、安全的 Token 刷新机制,避免用户频繁登录体验不佳。
解决方案
方案一:双 Token 模式(Access Token + Refresh Token)
设计思路
- 使用两个 Token:
- Access Token:短生命周期(如 15 分钟)。
- Refresh Token:长生命周期(如 7 天)。
- 客户端使用 Access Token 请求资源;当 Access Token 过期时,使用 Refresh Token 获取新的 Access Token 和 Refresh Token。
优点
- 高安全性:Access Token 即使泄露,时间窗口较短,风险可控。
- 客户端和服务器可以协同管理 Refresh Token,有效防止重复使用攻击。
缺点
- 客户端实现复杂度较高,需要管理两个 Token。
- 需要额外的接口和逻辑处理 Refresh Token。
适用场景
- 对安全性要求较高的系统,如金融、支付平台。
核心代码
后端接口验证 Refresh Token 并生成新 Token:
@PostMapping("/refresh-token")
public ResponseEntity<?> refreshToken(@RequestBody Map<String, String> tokens) {String refreshToken = tokens.get("refreshToken");if (!jwtUtil.checkToken(refreshToken)) {return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid Refresh Token");}Map<String, String> claims = jwtUtil.decode(refreshToken);String newAccessToken = jwtUtil.generateJwtToken(claims.get("username"), claims.get("userId"));String newRefreshToken = jwtUtil.generateRefreshToken(claims.get("username"), claims.get("userId"));return ResponseEntity.ok(Map.of("accessToken", newAccessToken, "refreshToken", newRefreshToken));
}
方案二:固定 Session ID + Token
设计思路
- 后端生成唯一的 Session ID 并与 Token 关联,将会话信息(如 Token 状态、用户信息)存储到 Redis 或数据库。
- 客户端请求时,携带 Session ID 和 Token,后端验证会话状态和 Token。
优点
- 可灵活控制会话状态,支持主动使某些 Session 失效。
- 后端可以精细化管理 Token 状态,无需频繁解析 JWT 签名。
缺点
- 依赖 Redis 或数据库存储会话信息,增加系统复杂度。
- 不完全无状态,分布式部署时需要额外处理会话同步。
适用场景
- 用户登录频繁、对会话状态要求高的系统。
核心代码
// 从 Redis 获取会话信息并验证 Token
SessionInfo sessionInfo = redisUtil.get("session_" + sessionId, SessionInfo.class);
if (sessionInfo == null || !sessionInfo.getToken().equals(token)) {return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Session Invalid");
}// Token 过期时刷新
if (jwtUtil.isTokenExpired(token)) {String newToken = jwtUtil.generateJwtToken(sessionInfo.getUsername(), sessionInfo.getUserId());sessionInfo.setToken(newToken);redisUtil.set("session_" + sessionId, sessionInfo);return ResponseEntity.ok(Map.of("newToken", newToken));
}
方案三:Token 无状态 + 客户端定时刷新
设计思路
- Token 完全无状态,仅存储签名信息和过期时间。
- 客户端定期检查 Token 的有效期,并在即将过期时主动请求刷新。
优点
- 无需后端存储任何会话信息,完全无状态。
- 简单高效,适合分布式系统。
缺点
- 客户端需要管理 Token 刷新逻辑,增加复杂度。
- 无法主动失效某个用户会话(如强制下线)。
适用场景
- 轻量级或低安全需求的系统,如小型工具或内部系统。
核心代码
客户端定时检查并刷新:
function checkTokenExpiry() {const token = localStorage.getItem('accessToken');const payload = JSON.parse(atob(token.split('.')[1]));const expiryTime = payload.exp * 1000;if (Date.now() > expiryTime - 5 * 60 * 1000) { // 提前 5 分钟刷新refreshToken();}
}function refreshToken() {const refreshToken = localStorage.getItem('refreshToken');fetch('/refresh-token', {method: 'POST',body: JSON.stringify({ refreshToken }),headers: { 'Content-Type': 'application/json' }}).then(response => response.json()).then(data => {localStorage.setItem('accessToken', data.accessToken);localStorage.setItem('refreshToken', data.refreshToken);});
}
方案四:Hybrid Token(混合 Token)
设计思路
- 将部分会话信息存储到 Token 中,而状态信息(如是否过期)存储在 Redis。
- Token 验证时仅检查无状态部分,必要时从 Redis 获取状态信息。
优点
- 兼顾无状态性和灵活性。
- 后端可动态管理会话状态,同时减少频繁访问存储。
缺点
- 实现较复杂,适合对性能要求较高的场景。
适用场景
- 高并发、大规模分布式系统,如社交平台、内容分发系统。
方案对比
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
双 Token 模式 | 高安全性;支持长生命周期管理 | 增加客户端复杂度 | 金融、支付等高安全系统 |
Session ID + Token | 灵活控制会话状态,支持主动失效 | 依赖 Redis 或数据库存储 | 登录频繁的系统 |
无状态 Token | 完全无状态,简单高效 | 客户端复杂度高,无法主动失效 | 内部工具、轻量级系统 |
Hybrid Token | 兼顾性能和灵活性 | 实现复杂 | 高并发分布式系统 |
结语
不同的方案适用于不同的场景,开发者在选择方案时需要考虑:
- 安全性:是否需要强安全性保障,如支持主动失效。
- 性能:是否需要减少后端存储依赖或高效处理。
- 复杂度:是否愿意增加客户端和后端的实现复杂性。
在实际开发中,可以结合业务需求和技术条件选择最适合的方案,甚至进行多方案组合,实现性能与安全的平衡。