一、问题背景:JWT认证的登录困境
在前后端分离架构中,JWT(JSON Web Token)因其无状态特性被广泛用于身份认证。但JWT的过期机制(通常1-2小时)导致用户频繁遇到”Token过期,请重新登录”的提示,尤其在移动端场景下严重影响用户体验。
传统解决方案存在明显缺陷:
- 前端定时刷新:无法准确预判Token过期时间
- 手动刷新按钮:增加用户操作负担
- 单一Token机制:刷新期间请求可能失败
双Token机制通过引入Access Token(短期)和Refresh Token(长期)的组合,配合Axios拦截器实现无感知刷新,成为解决该问题的主流方案。
二、双Token机制核心原理
1. Token生命周期管理
| Token类型 | 过期时间 | 用途 | 存储位置 |
|---|---|---|---|
| Access Token | 15-30分钟 | 接口访问权限 | HTTP Only Cookie |
| Refresh Token | 7-30天 | 获取新的Access Token | HttpOnly Secure Cookie |
2. 工作流程
- 用户登录后获取双Token
- 每次请求携带Access Token
- 当Access Token过期时:
- 拦截器捕获401响应
- 使用Refresh Token获取新Token
- 重试原始请求
- Refresh Token过期时跳转登录页
三、Axios实现步骤详解
1. 基础配置
import axios from 'axios';const api = axios.create({baseURL: 'https://api.example.com',timeout: 5000,withCredentials: true // 重要:允许跨域携带Cookie});
2. 请求拦截器(添加Token)
api.interceptors.request.use(config => {const accessToken = getCookie('access_token');if (accessToken) {config.headers.Authorization = `Bearer ${accessToken}`;}return config;},error => Promise.reject(error));
3. 响应拦截器(Token刷新逻辑)
let isRefreshing = false;let subscribers = [];function subscribeTokenRefresh(cb) {subscribers.push(cb);}function onRrefreshed(token) {subscribers.forEach(cb => cb(token));subscribers = [];}api.interceptors.response.use(response => response,async error => {const { config, response } = error;// 处理401错误if (response?.status === 401) {const refreshToken = getCookie('refresh_token');if (!refreshToken) {redirectToLogin();return;}// 防止重复刷新if (!isRefreshing) {isRefreshing = true;try {const res = await axios.post('/auth/refresh', {refresh_token: refreshToken}, { withCredentials: true });const newAccessToken = res.data.access_token;setCookie('access_token', newAccessToken, { maxAge: 900 });onRrefreshed(newAccessToken);} catch (refreshError) {redirectToLogin();return Promise.reject(refreshError);} finally {isRefreshing = false;}}// 订阅刷新事件return new Promise(resolve => {subscribeTokenRefresh(newToken => {config.headers.Authorization = `Bearer ${newToken}`;resolve(api(config));});});}return Promise.reject(error);});
四、关键实现细节
1. 并发请求处理
通过订阅模式解决多个并发请求同时触发Token刷新的问题:
- 第一个401请求触发刷新
- 后续请求进入等待队列
- 刷新完成后批量重试
2. Token存储方案
| 存储方式 | 安全性 | 适用场景 |
|---|---|---|
| HttpOnly Cookie | 高 | 传统Web应用 |
| localStorage | 中 | 需要跨标签页共享的场景 |
| Session Storage | 低 | 单页应用临时存储 |
推荐使用HttpOnly Cookie存储Refresh Token,防止XSS攻击。
3. 刷新时机优化
- 前端预检:在Token过期前5分钟主动刷新
- 后端配合:返回剩余有效时间(
exp字段) - 错误重试:设置最大重试次数(通常2次)
五、异常处理与边界情况
1. Refresh Token过期
function redirectToLogin() {clearTokens();window.location.href = '/login?expired=true';}
2. 网络异常处理
async function refreshToken() {try {// 实现同上} catch (error) {if (error.response?.status === 403) {// Refresh Token无效redirectToLogin();}throw error;}}
3. 跨域问题解决
确保后端配置CORS:
Access-Control-Allow-Origin: *Access-Control-Allow-Credentials: trueAccess-Control-Allow-Headers: Authorization, Content-Type
六、性能优化建议
- Token缓存:在内存中缓存最新Token,减少Cookie读取
- 请求去重:对相同URL的并发请求去重
- 节流控制:限制刷新频率(如每分钟最多1次)
- 监控告警:记录Token刷新失败事件
- 渐进式降级:在网络异常时使用本地缓存Token
七、安全增强措施
- Refresh Token轮换:每次刷新颁发新Token
- 设备指纹:绑定Token到特定设备
- IP限制:检测异常登录地点
- 短有效期:Access Token建议15-30分钟
- CSRF防护:配合CSRF Token使用
八、完整实现示例
// tokenManager.jsclass TokenManager {constructor() {this.refreshing = false;this.subscribers = [];}getAccessToken() {return getCookie('access_token');}async refreshTokens() {if (this.refreshing) {return new Promise(resolve => {this.subscribers.push(resolve);});}this.refreshing = true;try {const refreshToken = getCookie('refresh_token');const response = await axios.post('/auth/refresh', {refresh_token: refreshToken}, { withCredentials: true });const newAccessToken = response.data.access_token;setCookie('access_token', newAccessToken, { maxAge: 900 });this.subscribers.forEach(resolve => resolve(newAccessToken));this.subscribers = [];return newAccessToken;} finally {this.refreshing = false;}}}// axios配置const tokenManager = new TokenManager();api.interceptors.request.use(config => {const token = tokenManager.getAccessToken();if (token) {config.headers.Authorization = `Bearer ${token}`;}return config;});api.interceptors.response.use(response => response,async error => {const { config, response: errResponse } = error;if (errResponse?.status === 401) {try {const newToken = await tokenManager.refreshTokens();config.headers.Authorization = `Bearer ${newToken}`;return api(config);} catch (refreshError) {redirectToLogin();return Promise.reject(refreshError);}}return Promise.reject(error);});
九、总结与最佳实践
- Token设计:Access Token短有效期(15-30分钟),Refresh Token长有效期(7-30天)
- 存储安全:Refresh Token使用HttpOnly Cookie,Access Token可根据场景选择
- 并发控制:使用订阅模式处理并发刷新请求
- 错误处理:区分Token过期和其他401错误
- 性能监控:记录Token刷新成功率、耗时等指标
通过合理实现双Token自动刷新机制,可以显著提升用户体验,减少因Token过期导致的业务中断。实际开发中需根据具体业务场景调整Token有效期、存储方式等参数,并配合完善的监控体系确保系统稳定性。