告别频繁登录:Axios双Token刷新全攻略

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

一、核心痛点:传统Token机制的局限性

在Web开发中,JWT(JSON Web Token)因其无状态特性被广泛用于身份认证。但传统单Token机制存在致命缺陷:

  1. 过期即失效:Access Token过期后,用户必须重新登录
  2. 安全与体验的矛盾:延长过期时间降低安全性,缩短时间影响体验
  3. Refresh Token的局限性:单Refresh Token方案在失效时仍需中断操作

典型场景:用户填写长表单时Token过期,导致数据丢失和强制重登,这种体验灾难在B端系统尤为突出。

二、双Token机制:安全与体验的完美平衡

1. 机制原理

采用Access Token(短期) + Refresh Token(长期)的组合方案:

  • Access Token:有效期15-30分钟,用于API调用
  • Refresh Token:有效期7-30天,用于获取新Access Token
  • 静默刷新:在Access Token过期前自动获取新Token

2. 核心优势

指标 单Token方案 双Token方案
安全性 ★☆☆ ★★★
用户体验 ★☆☆ ★★★
服务器压力 ★★☆ ★★☆
实现复杂度 ★☆☆ ★★☆

三、Axios实现方案:拦截器+Promise链

1. 基础环境准备

  1. // axios实例配置
  2. const api = axios.create({
  3. baseURL: 'https://api.example.com',
  4. timeout: 10000
  5. });
  6. // Token存储(建议使用加密存储)
  7. const tokenStore = {
  8. accessToken: localStorage.getItem('access_token'),
  9. refreshToken: localStorage.getItem('refresh_token')
  10. };

2. 请求拦截器实现

  1. api.interceptors.request.use(
  2. config => {
  3. const now = Date.now() / 1000;
  4. // 检查Token是否存在且未过期(预留5分钟缓冲)
  5. if (tokenStore.accessToken &&
  6. (!tokenStore.expiresIn || tokenStore.expiresIn > now + 300)) {
  7. config.headers.Authorization = `Bearer ${tokenStore.accessToken}`;
  8. return config;
  9. }
  10. // 触发Token刷新
  11. return refreshToken().then(newTokens => {
  12. updateTokens(newTokens);
  13. config.headers.Authorization = `Bearer ${newTokens.accessToken}`;
  14. return config;
  15. });
  16. },
  17. error => Promise.reject(error)
  18. );

3. 响应拦截器处理

  1. api.interceptors.response.use(
  2. response => response,
  3. async error => {
  4. const originalRequest = error.config;
  5. // 401未授权且不是刷新请求时
  6. if (error.response.status === 401 &&
  7. !originalRequest._retry &&
  8. !originalRequest.url.includes('/refresh')) {
  9. originalRequest._retry = true;
  10. try {
  11. const newTokens = await refreshToken();
  12. updateTokens(newTokens);
  13. originalRequest.headers.Authorization = `Bearer ${newTokens.accessToken}`;
  14. return api(originalRequest);
  15. } catch (refreshError) {
  16. // 刷新失败处理
  17. handleAuthError(refreshError);
  18. return Promise.reject(refreshError);
  19. }
  20. }
  21. return Promise.reject(error);
  22. }
  23. );

4. 刷新Token核心逻辑

  1. async function refreshToken() {
  2. try {
  3. const response = await api.post('/auth/refresh', {
  4. refreshToken: tokenStore.refreshToken
  5. });
  6. const { accessToken, refreshToken, expiresIn } = response.data;
  7. return { accessToken, refreshToken, expiresIn: Date.now()/1000 + expiresIn };
  8. } catch (error) {
  9. // 处理刷新失败(如Refresh Token过期)
  10. clearTokens();
  11. throw new Error('认证失效,请重新登录');
  12. }
  13. }

四、高级优化策略

1. 并发请求处理

  1. let isRefreshing = false;
  2. let subscribers = [];
  3. api.interceptors.response.use(
  4. response => response,
  5. error => {
  6. // ...前序检查
  7. if (!isRefreshing) {
  8. isRefreshing = true;
  9. refreshToken().then(newTokens => {
  10. updateTokens(newTokens);
  11. subscribers.forEach(cb => cb(newTokens.accessToken));
  12. subscribers = [];
  13. });
  14. }
  15. return new Promise(resolve => {
  16. subscribers.push(accessToken => {
  17. originalRequest.headers.Authorization = `Bearer ${accessToken}`;
  18. resolve(api(originalRequest));
  19. });
  20. });
  21. }
  22. );

2. Token失效预测

  1. // 基于历史请求模式的预测算法
  2. class TokenPredictor {
  3. constructor() {
  4. this.requestHistory = [];
  5. this.maxHistory = 10;
  6. }
  7. recordRequest() {
  8. const now = Date.now();
  9. this.requestHistory.push(now);
  10. if (this.requestHistory.length > this.maxHistory) {
  11. this.requestHistory.shift();
  12. }
  13. }
  14. predictExpiration() {
  15. if (this.requestHistory.length < 3) return null;
  16. // 简单线性预测(实际可用更复杂的算法)
  17. const lastInterval = this.requestHistory[this.requestHistory.length-1] -
  18. this.requestHistory[this.requestHistory.length-2];
  19. return Date.now() + lastInterval * 1.5; // 预留1.5倍缓冲
  20. }
  21. }

五、安全增强措施

  1. Refresh Token轮换:每次刷新返回新Refresh Token
  2. 设备指纹绑定:将Token与设备信息关联
  3. 异常检测:监控短时间内频繁刷新行为
  4. HTTPS强制:所有Token传输必须加密

六、生产环境实践建议

  1. Token存储

    • Web端:HttpOnly Cookie + SameSite=Strict
    • 移动端:Keychain/Keystore加密存储
  2. 过期时间配置
    | 环境 | Access Token | Refresh Token |
    |——————|———————|———————-|
    | 开发环境 | 5分钟 | 1天 |
    | 生产环境 | 15分钟 | 7天 |
    | 高安全场景 | 5分钟 | 1小时 |

  3. 监控指标

    • Token刷新成功率
    • 静默刷新触发频率
    • 401错误发生率

七、完整实现示例

  1. // 完整封装示例
  2. class AuthAxios {
  3. constructor(baseURL) {
  4. this.api = axios.create({ baseURL });
  5. this.tokenStore = {
  6. accessToken: null,
  7. refreshToken: null,
  8. expiresIn: 0
  9. };
  10. this.initInterceptors();
  11. }
  12. initInterceptors() {
  13. // 请求拦截器
  14. this.api.interceptors.request.use(config => {
  15. if (this.isTokenValid()) {
  16. config.headers.Authorization = `Bearer ${this.tokenStore.accessToken}`;
  17. return config;
  18. }
  19. return this.refreshAndRetry(config);
  20. });
  21. // 响应拦截器
  22. this.api.interceptors.response.use(
  23. response => response,
  24. async error => {
  25. if (error.response.status === 401 && !error.config._retry) {
  26. error.config._retry = true;
  27. try {
  28. const newTokens = await this.refreshTokens();
  29. error.config.headers.Authorization = `Bearer ${newTokens.accessToken}`;
  30. return this.api(error.config);
  31. } catch (refreshError) {
  32. this.handleLogout();
  33. throw refreshError;
  34. }
  35. }
  36. throw error;
  37. }
  38. );
  39. }
  40. isTokenValid() {
  41. return this.tokenStore.accessToken &&
  42. this.tokenStore.expiresIn > Date.now()/1000 + 300;
  43. }
  44. async refreshTokens() {
  45. const response = await this.api.post('/auth/refresh', {
  46. refreshToken: this.tokenStore.refreshToken
  47. });
  48. const { accessToken, refreshToken, expiresIn } = response.data;
  49. this.updateTokens({
  50. accessToken,
  51. refreshToken,
  52. expiresIn: Date.now()/1000 + expiresIn
  53. });
  54. return { accessToken, expiresIn: this.tokenStore.expiresIn };
  55. }
  56. updateTokens(tokens) {
  57. Object.assign(this.tokenStore, tokens);
  58. // 实际项目应使用安全存储
  59. localStorage.setItem('access_token', tokens.accessToken);
  60. localStorage.setItem('refresh_token', tokens.refreshToken);
  61. }
  62. }

八、常见问题解决方案

  1. CSRF攻击防护

    • 使用Anti-CSRF Token
    • 实现SameSite Cookie属性
  2. 多标签页同步

    1. // 使用BroadcastChannel API
    2. const authChannel = new BroadcastChannel('auth');
    3. authChannel.onmessage = event => {
    4. if (event.data.type === 'token_update') {
    5. updateTokens(event.data.tokens);
    6. }
    7. };
  3. 移动端适配

    • 处理应用进入后台后的Token失效
    • 实现网络状态变化时的Token重试

九、性能优化指标

实施双Token机制后,应监控以下关键指标:

  1. 静默刷新成功率:目标>99.9%
  2. 用户感知中断率:目标<0.1%
  3. Token刷新延迟:中位数<200ms,P99<1s
  4. 服务器负载变化:Refresh Token请求占比<5%

通过这种精心设计的Axios双Token刷新机制,开发者可以彻底解决Web应用中的频繁登录问题,在保证系统安全性的同时,提供丝滑般的用户体验。实际项目实施时,建议结合具体业务场景进行参数调优,并通过A/B测试验证优化效果。