针对你提出的“双 Token 无感续登架构”我的回答是完全可以而且JWT是业界实现该架构的最佳实践方案。但需要特别注意的是RefreshToken 强烈建议存数据库Redis而 AccessToken 使用 JWT。这种“非对称存储”的组合才是生产环境的标准落地方式。以下是针对你需求的前端标准落地技术方案1. 令牌的具体JWT实现方案令牌存储方式载荷(Payload)核心字段过期策略AccessTokenJWT字符串不落库{ sub: userId, roles: [], exp: 1h }1小时无状态校验RefreshTokenJWT字符串 Redis缓存{ sub: userId, version: 123 }7天有状态校验存Redis为什么RefreshToken不纯粹用JWT因为JWT一旦签发在有效期内无法主动失效。如果用户修改密码或设备丢失旧的RefreshToken依然可用。将RefreshToken的jti唯一ID存入Redis并设置7天过期后端校验时先验JWT签名再查Redis是否存在且未失效这样就能实现“滑动续期”和“手动踢下线”。2. 前端无感续期的核心拦截器逻辑Axios示例前端只需关注401状态码和业务错误码如ERR_TOKEN_EXPIRED。核心是请求队列机制防止多个接口同时过期导致多次刷新。javascript// 刷新Token锁 let isRefreshing false; // 等待队列存储刷新期间失败的请求回调 let failedQueue []; const processQueue (error, token null) { failedQueue.forEach(prom { if (error) prom.reject(error); else prom.resolve(token); }); failedQueue []; }; // 响应拦截器 axios.interceptors.response.use( response response, async (error) { const { config, response } error; // 条件返回401 且 不是刷新接口本身 且 未重试过 if (response?.status 401 !config._retry) { // 防止重复刷新 if (isRefreshing) { return new Promise((resolve, reject) { failedQueue.push({ resolve, reject }); }).then(token { config.headers.Authorization Bearer ${token}; return axios(config); }).catch(err Promise.reject(err)); } config._retry true; isRefreshing true; const refreshToken localStorage.getItem(refresh_token); try { // 调用刷新接口携带 refreshToken通常放在Body或Header const res await axios.post(/auth/refresh, { refreshToken }); const { newAccessToken, newRefreshToken } res.data; // 更新本地存储 localStorage.setItem(access_token, newAccessToken); localStorage.setItem(refresh_token, newRefreshToken); // 更新等待队列中的请求头 processQueue(null, newAccessToken); // 重试当前失败的请求 config.headers.Authorization Bearer ${newAccessToken}; return axios(config); } catch (refreshError) { // RefreshToken彻底过期后端返回403跳转登录页 processQueue(refreshError, null); localStorage.clear(); window.location.href /login; return Promise.reject(refreshError); } finally { isRefreshing false; } } return Promise.reject(error); } );3. 刷新RefreshToken滑动的后端逻辑后端在刷新接口中不仅要生成新的AccessToken必须把新的RefreshToken也一起返回。java// 伪代码示例 public TokenPair refreshToken(String oldRefreshToken) { // 1. 校验旧RefreshToken的签名和Redis中是否存在 String jti parseJWT(oldRefreshToken).getId(); if (!redis.hasKey(refresh: jti)) { throw new ExpiredException(彻底过期); } // 2. 生成新的AccessToken1小时 String newAccess JWT.create().withExpiresAt(new Date(System.currentTimeMillis() 3600000)).sign(); // 3. **关键**生成全新的RefreshToken覆盖Redis中的旧值 String newRefresh JWT.create().withExpiresAt(new Date(System.currentTimeMillis() 7 * 24 * 3600000)).sign(); String newJti parseJWT(newRefresh).getId(); // 删除旧的Redis Key存入新的Key过期时间重置为7天 redis.delete(refresh: jti); redis.setex(refresh: newJti, 7 * 24 * 3600, userId); return new TokenPair(newAccess, newRefresh); }4. 为什么说这是“前端标准落地”内存与存储分离AccessToken放在内存或localStorage注意XSS风险推荐内存嗅探RefreshToken放在localStorage或httpOnly Cookie。并发控制通过isRefreshing锁和failedQueue队列确保100个接口同时401时只发起1次刷新请求极大节省服务器资源。静默续期用户在操作中途如果Token过期请求会被阻塞但界面无白屏刷新成功后请求自动重发用户完全无感知。⚠️ 特别提醒安全红线绝对不要在前端使用JWT解密RefreshToken来判断是否过期前端只负责携带和存储是否过期、是否滑动必须由后端根据Redis中的时间和版本来判断。前端拦截器只认HTTP状态码401/403。如果你需要我可以继续为你细化前端如何防止XSS窃取Token如使用内存存储Web Worker或者刷新Token接口的并发锁优化代码需要吗