一、前言:用户为什么总在"关键时刻掉线"?
你是否遇到过这些场景?
- 用户填写了 10 分钟的表单,点击提交却跳转到登录页
- App 后台切换回来,提示"登录已过期"
- 客服正在处理工单,突然被强制登出
根本原因 :登录态(Token/Session)过期,但系统未自动续期!
在用户体验至上的今天,"无感续期"已成为现代 Web 和移动端应用的标配。本文将带你彻底解决"状态登录刷新"问题,实现用户活跃时自动延长登录有效期,做到"用则不退,久不用则退"。
二、问题本质:固定过期 vs 滑动过期
| 策略 | 说明 | 用户体验 |
|---|---|---|
| 固定过期(Fixed TTL) | 登录后 2 小时强制过期,无论是否操作 | ❌ 差(活跃用户也会被踢) |
| ✅ 滑动过期(Sliding Expiration) | 每次请求都延长有效期(如最后 30 分钟内有操作就续期) | ✅ 好(活跃用户永不过期) |
📌 目标 :实现 滑动过期机制,让用户在持续使用时不被中断。
三、主流方案对比
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 1. 前端定时刷新 Token | 每隔 N 分钟调用 /refresh 接口 |
简单直观 | 空请求浪费资源;用户切后台仍刷新 |
| 2. 双 Token 机制(Access + Refresh) | Access Token 短期有效,Refresh Token 长期有效 | 安全性高 | 实现复杂;需管理两个 Token |
| ✅ 3. 滑动过期(Redis 自动续期) | 每次有效请求都延长 Redis 中 Token 的 TTL | 无额外请求;精准续期 | 需拦截器支持 |
📌 推荐方案 :方案 3(滑动过期) ------ 简洁、高效、用户体验最佳!
四、实战:基于 Redis 的滑动过期实现(Spring Boot)
1. 前提:已有 Token 登录体系
假设你已实现:
- 登录成功 → 生成 Token → 存入 Redis(TTL = 2 小时)
- 请求携带
Authorization: Bearer <token> - 拦截器校验 Token 是否存在
若未实现,可参考前文《基于 Redis 实现短信登录》
2. 核心思路:在拦截器中自动续期
java
@Component
public class TokenAuthInterceptor implements HandlerInterceptor {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String TOKEN_PREFIX = "login:token:";
private static final long MAX_EXPIRE_SECONDS = 2 * 3600; // 最大有效期:2小时
private static final long RENEW_THRESHOLD = 30 * 60; // 最后30分钟内才续期
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String token = extractToken(request);
if (token == null) {
writeUnauthorized(response, "缺少认证凭证");
return false;
}
String key = TOKEN_PREFIX + token;
Boolean exists = redisTemplate.hasKey(key);
if (!Boolean.TRUE.equals(exists)) {
writeUnauthorized(response, "登录已过期,请重新登录");
return false;
}
// ✅ 关键:滑动续期逻辑
Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);
if (ttl != null && ttl < RENEW_THRESHOLD) {
// 距离过期不足30分钟,延长至最大有效期
redisTemplate.expire(key, MAX_EXPIRE_SECONDS, TimeUnit.SECONDS);
System.out.println("Token 续期成功,新TTL: " + MAX_EXPIRE_SECONDS + "s");
}
// 可选:将用户信息放入 ThreadLocal
String userInfo = redisTemplate.opsForValue().get(key);
UserContext.setCurrentUser(parseUser(userInfo));
return true;
}
private String extractToken(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7);
}
return null;
}
// ... writeUnauthorized、parseUser 等方法省略
}
🔑 设计亮点:
- 仅在临近过期时续期,避免频繁写 Redis
- 每次有效请求都检查,确保活跃用户不掉线
- 无额外接口,对前端透明
3. 配置拦截器(放行登录等接口)
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private TokenAuthInterceptor tokenAuthInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenAuthInterceptor)
.addPathPatterns("/api/**")
.excludePathPatterns(
"/login",
"/sms-login",
"/send-sms-code",
"/captcha",
"/public/**"
);
}
}
五、前端配合(可选优化)
虽然服务端已实现无感续期,但前端仍可做体验优化:
1. 记录 Token 过期时间
java
// 登录成功后
const expire = res.data.expire; // 时间戳
localStorage.setItem('token_expire', expire);
// 请求拦截器:提前提示
axios.interceptors.request.use(config => {
const expire = localStorage.getItem('token_expire');
if (expire && Date.now() > expire - 5 * 60 * 1000) {
// 提前5分钟提示"即将过期"
showWarning("您的登录即将过期,请及时保存工作");
}
return config;
});
💡 注意:前端时间不可信,最终以服务端 Redis TTL 为准!
六、安全与性能考量
⚠️ 安全风险:无限续期?
- 不会 !因为:
- 每次续期最多延长到
MAX_EXPIRE_SECONDS - 用户长时间不操作(>2小时),Token 仍会过期
- 黑产无法通过刷请求无限续期(需合法 Token)
- 每次续期最多延长到
⚡ 性能影响:频繁 expire 操作?
- 极低 !因为:
- 仅当
TTL < 30分钟时才执行EXPIRE命令 - Redis 的
EXPIRE是 O(1) 操作 - 相比用户流失,这点开销微不足道
- 仅当
七、进阶:双 Token 机制(高安全场景)
若需更高安全性(如金融系统),可结合 Access Token + Refresh Token:
1. 登录返回:
- access_token(有效期 15 分钟)
- refresh_token(有效期 7 天)
2. access_token 过期后,前端用 refresh_token 换新 access_token
3. refresh_token 使用一次即失效(防重放)
但该方案复杂度高,普通业务推荐滑动过期即可。
八、避坑指南
❌ 坑 1:每次请求都无条件续期
后果 :Redis 写压力大,且用户"永远不退出"
正解 :只在临近过期时续期
❌ 坑 2:前端自行延长过期时间
风险 :绕过服务端控制,造成安全漏洞
正解 :所有过期逻辑由服务端 Redis 决定
❌ 坑 3:忘记放行登录接口
现象 :登录请求被拦截器拦截,死循环
解决 :务必 excludePathPatterns 放行 /login 等
九、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!