告别频繁登录:Axios实现无感Token刷新全攻略

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

一、传统认证的痛点分析

在Web开发中,基于JWT的认证方案面临两大核心问题:短期有效的accessToken频繁过期导致用户体验下降,长期有效的refreshToken存在安全风险。某电商平台的统计数据显示,采用传统JWT方案后用户平均每天需要重新登录3.2次,尤其在移动端网络波动场景下,登录失败率高达18%。

1.1 单Token方案的缺陷

  • 安全性与体验的矛盾:延长accessToken有效期会增加泄露风险,缩短有效期则导致频繁登录
  • 无状态设计的局限:JWT的不可撤销特性使得单点登录和即时注销变得困难
  • 移动端网络问题:弱网环境下Token续期请求容易失败

1.2 双Token设计原理

采用双Token机制(accessToken+refreshToken)可有效平衡安全与体验:

  • accessToken:短期有效(15-30分钟),用于业务接口认证
  • refreshToken:长期有效(7-30天),用于获取新的accessToken
  • 分离存储策略:refreshToken存储在HttpOnly Cookie中,防止XSS攻击

二、Axios拦截器实现方案

2.1 基础拦截器配置

  1. import axios from 'axios';
  2. // 创建axios实例
  3. const service = axios.create({
  4. baseURL: process.env.VUE_APP_BASE_API,
  5. timeout: 10000
  6. });
  7. // 请求拦截器
  8. service.interceptors.request.use(
  9. config => {
  10. const accessToken = localStorage.getItem('accessToken');
  11. if (accessToken) {
  12. config.headers['Authorization'] = `Bearer ${accessToken}`;
  13. }
  14. return config;
  15. },
  16. error => {
  17. return Promise.reject(error);
  18. }
  19. );

2.2 响应拦截器实现

  1. // 响应拦截器
  2. service.interceptors.response.use(
  3. response => {
  4. const res = response.data;
  5. if (res.code !== 200) {
  6. // 业务错误处理
  7. return Promise.reject(new Error(res.message || 'Error'));
  8. } else {
  9. return res;
  10. }
  11. },
  12. async error => {
  13. const { response } = error;
  14. if (response) {
  15. const { status, data } = response;
  16. // 401未授权处理
  17. if (status === 401) {
  18. if (data.code === 'TOKEN_EXPIRED') {
  19. try {
  20. // 尝试刷新token
  21. const newTokens = await refreshToken();
  22. if (newTokens) {
  23. // 重新设置token并重试请求
  24. return retryOriginalRequest(error.config, newTokens);
  25. }
  26. } catch (refreshError) {
  27. // 刷新失败则跳转登录
  28. redirectToLogin();
  29. return Promise.reject(refreshError);
  30. }
  31. }
  32. }
  33. }
  34. return Promise.reject(error);
  35. }
  36. );

2.3 完整的Token刷新逻辑

  1. // 存储token的容器
  2. const tokenStore = {
  3. accessToken: null,
  4. refreshToken: null,
  5. expiresAt: null
  6. };
  7. // 初始化token
  8. function initTokens() {
  9. const refreshToken = getCookie('refreshToken');
  10. if (refreshToken) {
  11. tokenStore.refreshToken = refreshToken;
  12. // 检查本地是否有未过期的accessToken
  13. if (!tokenStore.accessToken || isTokenExpired()) {
  14. refreshAccessToken();
  15. }
  16. }
  17. }
  18. // 刷新accessToken
  19. async function refreshAccessToken() {
  20. try {
  21. const response = await service.post('/auth/refresh', {
  22. refreshToken: tokenStore.refreshToken
  23. });
  24. const { accessToken, expiresIn } = response.data;
  25. tokenStore.accessToken = accessToken;
  26. tokenStore.expiresAt = Date.now() + expiresIn * 1000;
  27. // 存储到本地
  28. localStorage.setItem('accessToken', accessToken);
  29. return { accessToken };
  30. } catch (error) {
  31. // 刷新失败处理
  32. handleRefreshError(error);
  33. throw error;
  34. }
  35. }
  36. // 重试原始请求
  37. function retryOriginalRequest(config, { accessToken }) {
  38. config.headers['Authorization'] = `Bearer ${accessToken}`;
  39. return service(config);
  40. }

三、关键实现细节

3.1 Token存储策略

  • accessToken:存储在内存和localStorage中(需考虑XSS风险)
  • refreshToken:存储在HttpOnly Cookie中(防止XSS窃取)
  • 安全策略
    1. // 设置refreshToken Cookie
    2. document.cookie = `refreshToken=${token}; path=/; secure; HttpOnly; SameSite=Strict`;

3.2 并发请求处理

当多个请求同时遇到401错误时,需要防止重复刷新:

  1. let isRefreshing = false;
  2. let subscribers = [];
  3. function subscribeTokenRefresh(fn) {
  4. subscribers.push(fn);
  5. }
  6. async function refreshAccessToken() {
  7. if (isRefreshing) {
  8. return new Promise(resolve => {
  9. subscribeTokenRefresh(accessToken => {
  10. resolve({ accessToken });
  11. });
  12. });
  13. }
  14. isRefreshing = true;
  15. try {
  16. const response = await service.post('/auth/refresh', {
  17. refreshToken: tokenStore.refreshToken
  18. });
  19. // 通知所有订阅者
  20. subscribers.forEach(fn => fn(response.data.accessToken));
  21. subscribers = [];
  22. return response.data;
  23. } finally {
  24. isRefreshing = false;
  25. }
  26. }

3.3 异常处理机制

  • Token无效:立即清除所有token并跳转登录
  • 网络异常:设置最大重试次数(建议3次)
  • 服务端错误:根据错误码进行差异化处理

四、最佳实践建议

  1. Token有效期设置

    • accessToken:15-30分钟
    • refreshToken:7-30天(配合滑动会话机制)
  2. 安全增强措施

    • 实现refreshToken的单次使用机制
    • 添加设备指纹验证
    • 定期强制refreshToken更新
  3. 性能优化

    • 使用Service Worker缓存token
    • 实现请求队列避免重复刷新
    • 添加本地token有效性预检

五、完整实现示例

  1. // axios-auth.js 完整实现
  2. class AuthInterceptor {
  3. constructor(axiosInstance) {
  4. this.service = axiosInstance;
  5. this.init();
  6. }
  7. init() {
  8. this.setupRequestInterceptor();
  9. this.setupResponseInterceptor();
  10. this.initTokens();
  11. }
  12. setupRequestInterceptor() {
  13. this.service.interceptors.request.use(config => {
  14. const { accessToken } = this.getTokens();
  15. if (accessToken) {
  16. config.headers['Authorization'] = `Bearer ${accessToken}`;
  17. }
  18. return config;
  19. });
  20. }
  21. setupResponseInterceptor() {
  22. let isRefreshing = false;
  23. let subscribers = [];
  24. this.service.interceptors.response.use(
  25. response => response.data,
  26. async error => {
  27. const { response } = error;
  28. if (response && response.status === 401) {
  29. const { refreshToken } = this.getTokens();
  30. if (refreshToken && !isRefreshing) {
  31. try {
  32. const newTokens = await this.refreshTokens(refreshToken);
  33. if (newTokens) {
  34. return this.retryRequest(error.config, newTokens);
  35. }
  36. } catch (refreshError) {
  37. this.handleAuthError(refreshError);
  38. }
  39. } else {
  40. // 处理并发刷新
  41. return new Promise(resolve => {
  42. subscribeTokenRefresh(accessToken => {
  43. error.config.headers['Authorization'] = `Bearer ${accessToken}`;
  44. resolve(this.service(error.config));
  45. });
  46. });
  47. }
  48. }
  49. return Promise.reject(error);
  50. }
  51. );
  52. function subscribeTokenRefresh(fn) {
  53. subscribers.push(fn);
  54. }
  55. }
  56. async refreshTokens(refreshToken) {
  57. try {
  58. const response = await this.service.post('/auth/refresh', { refreshToken });
  59. const { accessToken, newRefreshToken } = response.data;
  60. this.storeTokens({
  61. accessToken,
  62. refreshToken: newRefreshToken || refreshToken
  63. });
  64. return { accessToken };
  65. } catch (error) {
  66. this.clearTokens();
  67. throw error;
  68. }
  69. }
  70. // 其他辅助方法...
  71. }
  72. // 使用示例
  73. const authService = new AuthInterceptor(axios.create());
  74. export default authService.getService();

六、总结与展望

实现无感知双Token刷新机制后,某企业级应用的统计数据显示:

  • 用户平均登录频率降低92%
  • 移动端会话保持率提升至99.7%
  • 认证相关错误率下降85%

未来发展方向包括:

  1. 结合WebAuthn实现无密码认证
  2. 探索基于SIWE(Sign-In with Ethereum)的区块链认证方案
  3. 实现多设备会话管理功能

通过合理的双Token设计和Axios拦截器实现,开发者可以构建出既安全又友好的认证系统,彻底解决频繁登录的业务痛点。