告别频繁登录:教你用Axios实现无感知双Token刷新
一、问题背景:为何需要双Token刷新机制
在传统Web应用中,JWT(JSON Web Token)因其无状态特性被广泛用于身份认证。但单Token设计存在致命缺陷:当Token过期时,用户必须重新登录,尤其在SPA(单页应用)中体验极差。
典型痛点场景:
- 用户填写复杂表单时突然弹出登录框
- 视频播放中因Token过期中断
- 移动端网络不稳定时频繁触发401错误
双Token机制通过引入refreshToken(刷新令牌)完美解决这一问题:
- accessToken:短期有效(如15分钟),用于访问API
- refreshToken:长期有效(如7天),用于获取新accessToken
二、核心原理:Axios拦截器实现无感知刷新
1. Token存储与初始化
// token存储示例(建议使用http-only cookie或加密localStorage)const TOKEN_KEY = {ACCESS: 'access_token',REFRESH: 'refresh_token'};// 初始化axios实例const api = axios.create({baseURL: 'https://api.example.com',timeout: 5000});
2. 请求拦截器:自动附加Token
api.interceptors.request.use(config => {const accessToken = localStorage.getItem(TOKEN_KEY.ACCESS);if (accessToken) {config.headers.Authorization = `Bearer ${accessToken}`;}return config;},error => Promise.reject(error));
3. 响应拦截器:处理Token过期
let isRefreshing = false;let subscribers = [];api.interceptors.response.use(response => response,async error => {const { config, response } = error;// 401未授权错误处理if (response && response.status === 401) {// 防止重复刷新if (!isRefreshing) {isRefreshing = true;try {const refreshToken = localStorage.getItem(TOKEN_KEY.REFRESH);const { data } = await api.post('/auth/refresh', {refreshToken});// 更新存储的TokenlocalStorage.setItem(TOKEN_KEY.ACCESS, data.accessToken);// 重试原始请求subscribers.forEach(cb => cb());subscribers = [];return api(config);} catch (refreshError) {// 刷新失败则跳转登录window.location.href = '/login';return Promise.reject(refreshError);} finally {isRefreshing = false;}}// 等待刷新时存储重试请求return new Promise(resolve => {subscribers.push(() => {config.headers.Authorization = `Bearer ${localStorage.getItem(TOKEN_KEY.ACCESS)}`;resolve(api(config));});});}return Promise.reject(error);});
三、进阶实现:安全与优化策略
1. Token安全存储方案
| 存储方式 | 安全性 | 实现复杂度 | 适用场景 |
|---|---|---|---|
| http-only Cookie | ★★★★★ | ★ | 传统Web应用 |
| 加密localStorage | ★★★★ | ★★★ | SPA应用 |
| Memory Storage | ★ | ★ | 临时测试环境 |
2. 刷新Token失效处理
// 增强版刷新逻辑async function refreshToken() {try {const refreshToken = getRefreshToken();if (!refreshToken) throw new Error('No refresh token');const { data } = await api.post('/auth/refresh', { refreshToken });// 验证新Token有效性const testResponse = await api.get('/auth/verify', {headers: { Authorization: `Bearer ${data.accessToken}` }});if (testResponse.status !== 200) {throw new Error('Invalid new access token');}return data.accessToken;} catch (error) {clearAllTokens();throw error;}}
3. 并发请求优化
// 使用Promise.all处理并发刷新async function handleConcurrentRefresh(requests) {try {const newAccessToken = await refreshToken();return requests.map(req => {req.config.headers.Authorization = `Bearer ${newAccessToken}`;return api(req.config);});} catch (error) {return requests.map(() => Promise.reject(error));}}
四、最佳实践:完整实现示例
1. Token服务封装
class TokenService {static getAccessToken() {return localStorage.getItem(TOKEN_KEY.ACCESS);}static setTokens({ accessToken, refreshToken }) {localStorage.setItem(TOKEN_KEY.ACCESS, accessToken);localStorage.setItem(TOKEN_KEY.REFRESH, refreshToken);}static clearTokens() {localStorage.removeItem(TOKEN_KEY.ACCESS);localStorage.removeItem(TOKEN_KEY.REFRESH);}static async refresh() {const refreshToken = this.getRefreshToken();if (!refreshToken) throw new Error('No refresh token');const { data } = await api.post('/auth/refresh', { refreshToken });this.setTokens(data);return data.accessToken;}}
2. 增强版Axios配置
const enhancedApi = axios.create({// ...基础配置});let refreshLock = false;let retryQueue = [];enhancedApi.interceptors.response.use(response => response,async error => {const { config, response } = error;if (response?.status === 401) {if (!refreshLock) {refreshLock = true;try {const newAccessToken = await TokenService.refresh();// 重放所有排队请求retryQueue.forEach(cb => cb(newAccessToken));retryQueue = [];// 重试当前请求config.headers.Authorization = `Bearer ${newAccessToken}`;return enhancedApi(config);} catch (refreshError) {TokenService.clearTokens();window.location.href = '/login';return Promise.reject(refreshError);} finally {refreshLock = false;}}// 排队等待刷新return new Promise(resolve => {retryQueue.push(newAccessToken => {config.headers.Authorization = `Bearer ${newAccessToken}`;resolve(enhancedApi(config));});});}return Promise.reject(error);});
五、部署注意事项
- CORS配置:确保刷新端点允许来自前端域的请求
- Token过期时间:建议accessToken设置15-30分钟,refreshToken设置7-30天
- CSRF防护:使用SameSite Cookie属性增强安全性
- 日志监控:记录Token刷新失败事件以便排查问题
- 移动端适配:考虑网络不稳定情况下的重试机制
六、效果对比
| 指标 | 单Token方案 | 双Token方案 |
|---|---|---|
| 用户中断次数 | 高(每次过期) | 极低(无感知刷新) |
| 开发复杂度 | 低 | 中等 |
| 安全性 | 中等 | 高(refreshToken可控) |
| 适用场景 | 简单内部系统 | 用户型Web/移动应用 |
七、常见问题解答
Q1:refreshToken被盗用怎么办?
A:设置refreshToken的短有效期(如1天),结合设备指纹验证,发现异常立即失效所有Token。
Q2:如何处理多个标签页的Token刷新?
A:使用BroadcastChannel API或localStorage事件监听实现跨标签页同步。
Q3:微前端架构如何适配?
A:将Token服务封装为独立模块,通过全局事件或状态管理共享Token状态。
通过实施上述方案,您的Web应用将实现真正的无感知会话管理,用户再也不会因Token过期而被强制登录,大幅提升用户体验和系统稳定性。