Axios双Token实战:实现无感知刷新告别频繁登录

一、问题背景: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. 工作流程

  1. 用户登录后获取双Token
  2. 每次请求携带Access Token
  3. 当Access Token过期时:
    • 拦截器捕获401响应
    • 使用Refresh Token获取新Token
    • 重试原始请求
  4. Refresh Token过期时跳转登录页

三、Axios实现步骤详解

1. 基础配置

  1. import axios from 'axios';
  2. const api = axios.create({
  3. baseURL: 'https://api.example.com',
  4. timeout: 5000,
  5. withCredentials: true // 重要:允许跨域携带Cookie
  6. });

2. 请求拦截器(添加Token)

  1. api.interceptors.request.use(
  2. config => {
  3. const accessToken = getCookie('access_token');
  4. if (accessToken) {
  5. config.headers.Authorization = `Bearer ${accessToken}`;
  6. }
  7. return config;
  8. },
  9. error => Promise.reject(error)
  10. );

3. 响应拦截器(Token刷新逻辑)

  1. let isRefreshing = false;
  2. let subscribers = [];
  3. function subscribeTokenRefresh(cb) {
  4. subscribers.push(cb);
  5. }
  6. function onRrefreshed(token) {
  7. subscribers.forEach(cb => cb(token));
  8. subscribers = [];
  9. }
  10. api.interceptors.response.use(
  11. response => response,
  12. async error => {
  13. const { config, response } = error;
  14. // 处理401错误
  15. if (response?.status === 401) {
  16. const refreshToken = getCookie('refresh_token');
  17. if (!refreshToken) {
  18. redirectToLogin();
  19. return;
  20. }
  21. // 防止重复刷新
  22. if (!isRefreshing) {
  23. isRefreshing = true;
  24. try {
  25. const res = await axios.post('/auth/refresh', {
  26. refresh_token: refreshToken
  27. }, { withCredentials: true });
  28. const newAccessToken = res.data.access_token;
  29. setCookie('access_token', newAccessToken, { maxAge: 900 });
  30. onRrefreshed(newAccessToken);
  31. } catch (refreshError) {
  32. redirectToLogin();
  33. return Promise.reject(refreshError);
  34. } finally {
  35. isRefreshing = false;
  36. }
  37. }
  38. // 订阅刷新事件
  39. return new Promise(resolve => {
  40. subscribeTokenRefresh(newToken => {
  41. config.headers.Authorization = `Bearer ${newToken}`;
  42. resolve(api(config));
  43. });
  44. });
  45. }
  46. return Promise.reject(error);
  47. }
  48. );

四、关键实现细节

1. 并发请求处理

通过订阅模式解决多个并发请求同时触发Token刷新的问题:

  • 第一个401请求触发刷新
  • 后续请求进入等待队列
  • 刷新完成后批量重试

2. Token存储方案

存储方式 安全性 适用场景
HttpOnly Cookie 传统Web应用
localStorage 需要跨标签页共享的场景
Session Storage 单页应用临时存储

推荐使用HttpOnly Cookie存储Refresh Token,防止XSS攻击。

3. 刷新时机优化

  • 前端预检:在Token过期前5分钟主动刷新
  • 后端配合:返回剩余有效时间(exp字段)
  • 错误重试:设置最大重试次数(通常2次)

五、异常处理与边界情况

1. Refresh Token过期

  1. function redirectToLogin() {
  2. clearTokens();
  3. window.location.href = '/login?expired=true';
  4. }

2. 网络异常处理

  1. async function refreshToken() {
  2. try {
  3. // 实现同上
  4. } catch (error) {
  5. if (error.response?.status === 403) {
  6. // Refresh Token无效
  7. redirectToLogin();
  8. }
  9. throw error;
  10. }
  11. }

3. 跨域问题解决

确保后端配置CORS:

  1. Access-Control-Allow-Origin: *
  2. Access-Control-Allow-Credentials: true
  3. Access-Control-Allow-Headers: Authorization, Content-Type

六、性能优化建议

  1. Token缓存:在内存中缓存最新Token,减少Cookie读取
  2. 请求去重:对相同URL的并发请求去重
  3. 节流控制:限制刷新频率(如每分钟最多1次)
  4. 监控告警:记录Token刷新失败事件
  5. 渐进式降级:在网络异常时使用本地缓存Token

七、安全增强措施

  1. Refresh Token轮换:每次刷新颁发新Token
  2. 设备指纹:绑定Token到特定设备
  3. IP限制:检测异常登录地点
  4. 短有效期:Access Token建议15-30分钟
  5. CSRF防护:配合CSRF Token使用

八、完整实现示例

  1. // tokenManager.js
  2. class TokenManager {
  3. constructor() {
  4. this.refreshing = false;
  5. this.subscribers = [];
  6. }
  7. getAccessToken() {
  8. return getCookie('access_token');
  9. }
  10. async refreshTokens() {
  11. if (this.refreshing) {
  12. return new Promise(resolve => {
  13. this.subscribers.push(resolve);
  14. });
  15. }
  16. this.refreshing = true;
  17. try {
  18. const refreshToken = getCookie('refresh_token');
  19. const response = await axios.post('/auth/refresh', {
  20. refresh_token: refreshToken
  21. }, { withCredentials: true });
  22. const newAccessToken = response.data.access_token;
  23. setCookie('access_token', newAccessToken, { maxAge: 900 });
  24. this.subscribers.forEach(resolve => resolve(newAccessToken));
  25. this.subscribers = [];
  26. return newAccessToken;
  27. } finally {
  28. this.refreshing = false;
  29. }
  30. }
  31. }
  32. // axios配置
  33. const tokenManager = new TokenManager();
  34. api.interceptors.request.use(config => {
  35. const token = tokenManager.getAccessToken();
  36. if (token) {
  37. config.headers.Authorization = `Bearer ${token}`;
  38. }
  39. return config;
  40. });
  41. api.interceptors.response.use(
  42. response => response,
  43. async error => {
  44. const { config, response: errResponse } = error;
  45. if (errResponse?.status === 401) {
  46. try {
  47. const newToken = await tokenManager.refreshTokens();
  48. config.headers.Authorization = `Bearer ${newToken}`;
  49. return api(config);
  50. } catch (refreshError) {
  51. redirectToLogin();
  52. return Promise.reject(refreshError);
  53. }
  54. }
  55. return Promise.reject(error);
  56. }
  57. );

九、总结与最佳实践

  1. Token设计:Access Token短有效期(15-30分钟),Refresh Token长有效期(7-30天)
  2. 存储安全:Refresh Token使用HttpOnly Cookie,Access Token可根据场景选择
  3. 并发控制:使用订阅模式处理并发刷新请求
  4. 错误处理:区分Token过期和其他401错误
  5. 性能监控:记录Token刷新成功率、耗时等指标

通过合理实现双Token自动刷新机制,可以显著提升用户体验,减少因Token过期导致的业务中断。实际开发中需根据具体业务场景调整Token有效期、存储方式等参数,并配合完善的监控体系确保系统稳定性。