背景问题:为什么需要无感刷新?
想象这样一个场景:
“我正在后台管理系统中录入数据,页面突然跳转回登录界面,之前填写的内容全没了!”
这是典型的 Token 到期导致会话失效 的问题,尤其在使用 Redis 等缓存中间件存储 Token 时尤为常见。
问题根源
后端通常通过 JWT 来实现无状态身份验证,但 JWT 的缺陷也很明显:过期即失效,无法修改或撤销。如果不设计 Token 刷新机制,用户体验将大打折扣。
核心策略:Token 无感续签方案概述
方案一:后端自动续期(推荐)
在每次用户请求时,后端检查当前 Token 的有效时间:
若临近过期(如小于5分钟) ,则动态生成一个新 Token,加入响应头中返回;
前端拦截响应头,若发现新的 Token,与本地不一致则自动更新本地 Token。
方案二:前端主动续签(补充方案)
前端维护一对 Token:access_token(短期)+ refresh_token(长期);
每隔一段时间,前端使用 refresh_token 去调用刷新接口,获取新的 access_token。
后端实现细节
依赖配置(pom.xml)
<dependencies>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
JWT 工具类 JwtUtil.java
代码路径:/src/main/java/com/demo/auth/utils/JwtUtil.java
package com.demo.auth.utils;
import io.jsonwebtoken.*;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;
public class JwtUtil {
public static final long JWT_TTL = 1000L * 60 * 60 * 24; // 24小时
public static final String JWT_KEY = "qx";
public static String createJWT(String subject) {
return getJwtBuilder(subject, null, UUID.randomUUID().toString().replace("-", "")).compact();
}
public static String createJWT(String subject, Long ttlMillis) {
return getJwtBuilder(subject, ttlMillis, UUID.randomUUID().toString()).compact();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
long nowMillis = System.currentTimeMillis();
long expMillis = (ttlMillis != null ? nowMillis + ttlMillis : nowMillis + JWT_TTL);
SecretKey secretKey = generalKey();
return Jwts.builder()
.setId(uuid)
.setSubject(subject)
.setIssuer("icoderoad")
.setIssuedAt(new Date(nowMillis))
.setExpiration(new Date(expMillis))
.signWith(SignatureAlgorithm.HS256, secretKey);
}
public static Claims parseJWT(String jwt) throws Exception {
return Jwts.parser()
.setSigningKey(generalKey())
.parseClaimsJws(jwt)
.getBody();
}
public static SecretKey generalKey() {
byte[] key = Base64.getDecoder().decode(JWT_KEY);
return new SecretKeySpec(key, 0, key.length, "AES");
}
public static Date getExpiration(String jwt) {
try {
return parseJWT(jwt).getExpiration();
} catch (Exception e) {
throw new RuntimeException("Token 解析失败", e);
}
}
}
Token 拦截与续签逻辑
拦截器路径:/src/main/java/com/demo/auth/interceptor/AuthInterceptor.java
public class AuthInterceptor implements HandlerInterceptor {
private static final long REFRESH_THRESHOLD = 1000L * 60 * 5; // 剩余5分钟内刷新
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (StringUtils.isEmpty(token)) {
throw new RuntimeException("未登录");
}
Claims claims = JwtUtil.parseJWT(token);
long now = System.currentTimeMillis();
long exp = claims.getExpiration().getTime();
if (exp - now < REFRESH_THRESHOLD) {
String newToken = JwtUtil.createJWT(claims.getSubject());
response.setHeader("X-Token-Refresh", newToken);
}
return true;
}
}
前端处理逻辑(以 Vue + Axios 为例)
前端拦截代码:
axios.interceptors.response.use(response => {
const newToken = response.headers['x-token-refresh'];
if (newToken && newToken !== localStorage.getItem('access_token')) {
localStorage.setItem('access_token', newToken);
}
return response;
}, error => {
// 处理401
if (error.response.status === 401) {
// 可以保存草稿后跳转登录
}
return Promise.reject(error);
});
关于 AccessToken 和 RefreshToken 的机制说明
类型 | 用途 | 特点 |
---|---|---|
AccessToken | 携带用户身份,频繁使用 | 安全风险高,需短时过期 |
RefreshToken | 用于续签 AccessToken | 不暴露给前端,一般保存在 Cookie 或 HttpOnly |
标准双 Token 模式提升了安全性和用户体验,避免因 AccessToken 频繁刷新带来的资源浪费。
特别讨论:表单静默超时的处理策略
场景问题:
用户长时间填写表单,没有发出任何请求,点击提交时发现 token 已失效,被重定向到登录页,数据全丢。
推荐方案:
提交失败后前端本地缓存表单数据;
登录成功后回显草稿,确保用户体验不受损;
或者在用户输入行为时定期心跳请求,触发后端续签。
总结
实现无感刷新 Token,是用户体验与安全性协同优化的重要实践。通过后端智能判断与前端拦截配合,结合双 Token 模式或动态续签机制,我们可以实现:
用户操作不中断 身份凭证自动续期 安全控制粒度更灵活
作者:星辰聊技术
链接:https://juejin.cn