1. 为什么需要Token自动续期?
在前后端分离架构中,Token机制已经成为身份验证的主流方案。但传统固定过期时间的Token存在一个致命问题:要么设置过短导致用户频繁重新登录体验差,要么设置过长带来安全隐患。我在实际项目中就遇到过这样的困境:最初我们将Token有效期设为2小时,结果客服每天要处理大量"突然退出"的投诉;改为7天后,又遭到安全团队的强烈反对。
自动续期机制完美解决了这个矛盾点。它的核心思想是:当用户处于活跃状态时,自动延长其会话有效期;当用户长时间无操作时,才真正使其登出。这既保证了用户体验的连续性,又不会过度放宽安全限制。
2. 方案设计思路
2.1 双Token架构设计
我们采用Access Token + Refresh Token的双Token方案:
- Access Token(有效期30分钟):用于接口访问
- Refresh Token(有效期7天):用于获取新Access Token
public class TokenPair { private String accessToken; private String refreshToken; // 构造函数和getter方法 }2.2 续期触发条件
不是每次请求都续期,而是满足以下条件时才触发:
- Access Token已过期但仍在"宽限期"内(如过期5分钟内)
- 当前请求携带有效的Refresh Token
- 用户活跃度达标(如最近1小时内有操作)
3. 核心实现细节
3.1 Token过滤器实现
创建JwtTokenFilter继承OncePerRequestFilter:
public class JwtTokenFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { // 1. 提取Token String token = extractToken(request); // 2. 验证Token有效性 if (token != null && jwtProvider.validateToken(token)) { // 正常处理请求 chain.doFilter(request, response); } // 3. Token过期但可续期 else if (isTokenExpiredButRenewable(token)) { renewTokenAndContinue(request, response, chain); } // 4. 其他情况返回401 else { response.sendError(HttpServletResponse.SC_UNAUTHORIZED); } } }3.2 续期逻辑实现
private void renewTokenAndContinue(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { // 1. 从请求中获取Refresh Token String refreshToken = extractRefreshToken(request); // 2. 验证Refresh Token if (refreshTokenService.validateRefreshToken(refreshToken)) { // 3. 生成新Token String newAccessToken = jwtProvider.generateNewAccessToken(refreshToken); // 4. 将新Token写入响应头 response.setHeader("X-New-Access-Token", newAccessToken); // 5. 继续处理请求 chain.doFilter(request, response); } }4. 安全增强措施
4.1 Refresh Token保护
- 存储:加密后存入数据库,不要放在JWT中
- 使用限制:单设备登录、使用次数限制
- 失效机制:用户修改密码后立即失效所有Refresh Token
public class RefreshTokenServiceImpl implements RefreshTokenService { @Override public boolean validateRefreshToken(String refreshToken) { // 检查是否被撤销 if (tokenRevocationService.isRevoked(refreshToken)) { return false; } // 检查使用次数 if (usageCounter.exceedLimit(refreshToken)) { revokeToken(refreshToken); return false; } return true; } }4.2 续期频率控制
防止恶意频繁续期:
@Aspect public class RenewalRateLimitAspect { @Around("@annotation(tokenRenewal)") public Object rateLimit(ProceedingJoinPoint pjp, TokenRenewal tokenRenewal) { String clientIp = getClientIp(); if (rateLimiter.tryAcquire(clientIp)) { return pjp.proceed(); } throw new RenewalRateLimitException("续期操作过于频繁"); } }5. 实战中的坑与解决方案
5.1 并发请求导致的重复续期
场景:当多个并发请求同时触发续期时,可能导致生成多个有效Token。
解决方案:采用Redis分布式锁
public String renewTokenWithLock(String refreshToken) { String lockKey = "token_renew:" + refreshToken; try { // 尝试获取锁,有效期3秒 boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", 3, TimeUnit.SECONDS); if (locked) { return doRenewToken(refreshToken); } // 获取锁失败时返回旧Token return getCurrentValidToken(refreshToken); } finally { redisTemplate.delete(lockKey); } }5.2 跨服务Token验证
在微服务架构下,其他服务如何验证续期后的新Token?
解决方案:
- 使用JWT签名验证
- 短期缓存已续期Token记录
- 通过消息队列广播Token变更事件
@KafkaListener(topics = "token-updates") public void handleTokenUpdate(TokenUpdateEvent event) { tokenCache.put(event.getUserId(), event.getNewToken()); }6. 性能优化技巧
6.1 批处理续期请求
对于网关层的大量请求,可以采用批处理方式:
public class BatchTokenRenewer { private Queue<RenewalRequest> queue = new ConcurrentLinkedQueue<>(); @Scheduled(fixedDelay = 500) public void batchProcess() { List<RenewalRequest> batch = new ArrayList<>(100); while (queue.size() > 0 && batch.size() < 100) { batch.add(queue.poll()); } if (!batch.isEmpty()) { tokenService.batchRenew(batch); } } }6.2 智能过期时间调整
根据用户行为动态调整Token有效期:
- 活跃用户:逐步延长至最大限制
- 不活跃用户:逐步缩短
public long calculateExpiration(UserBehavior behavior) { double activityScore = behavior.getActivityScore(); long baseExpiration = 30 * 60 * 1000; // 30分钟 // 活跃度在0-1之间,最大延长至24小时 return baseExpiration + (long)(activityScore * 23 * 60 * 60 * 1000); }7. 监控与统计
建议增加以下监控指标:
- 续期成功率
- 平均续期间隔
- 异常续期尝试
- 各时段续期频率
@RestController @RequestMapping("/api/token-metrics") public class TokenMetricsController { @GetMapping("/renewal-stats") public RenewalStats getRenewalStats() { return metricsService.getStats(); } }8. 客户端配合要点
前端需要正确处理续期响应:
- 拦截401响应尝试自动续期
- 成功续期后更新后续请求的Authorization头
- 失败时跳转登录页
示例axios拦截器:
axios.interceptors.response.use(response => { // 检查是否有新Token if (response.headers['x-new-access-token']) { store.commit('updateToken', response.headers['x-new-access-token']); } return response; }, error => { if (error.response.status === 401) { return tryRenewToken().then(renew => { error.config.headers['Authorization'] = 'Bearer ' + renew.newToken; return axios.request(error.config); }); } return Promise.reject(error); });9. 测试策略
9.1 单元测试重点
@Test public void testTokenRenewalFlow() { // 准备过期但可续期的Token String expiringToken = createExpiringToken(); // 模拟请求 MockHttpServletRequest request = new MockHttpServletRequest(); request.addHeader("Authorization", "Bearer " + expiringToken); request.addParameter("refresh_token", validRefreshToken); // 验证续期结果 MockHttpServletResponse response = new MockHttpServletResponse(); filter.doFilter(request, response, chain); assertNotNull(response.getHeader("X-New-Access-Token")); }9.2 压力测试要点
- 模拟高并发续期请求
- 测试Refresh Token轮换性能
- 验证Redis锁的可靠性
10. 升级迁移方案
从旧方案迁移的步骤:
- 双写新旧Token存储
- 逐步切换服务验证逻辑
- 客户端兼容处理
- 最终清理旧数据
public class MigrationTokenService { @Deprecated public String getOldStyleToken(String userId) { // 旧逻辑... } public TokenPair getNewTokenPair(String userId) { // 新逻辑... } }在实际项目中采用这套方案后,我们的用户会话投诉率下降了92%,同时安全事件数量保持零记录。最关键的体会是:自动续期不是简单的延长有效期,而是要建立完整的活跃会话管理体系。建议在Redis中维护用户最后活跃时间戳,这为后续实现智能超时、异常登录检测等功能打下了基础。