普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月16日首页

自动刷新token登录

2026年1月16日 09:36

刷新 Token 逻辑说明

1. 状态管理(第40-48行)

let isRefreshing = false; // 是否正在刷新token
let refreshTokenRetryCount = 0; // token刷新重试次数
const maxRefreshRetryCount = 2; // 最大重试次数
const pendingRequests = []; // 等待token刷新完成的请求队列
  • isRefreshing:标记是否正在刷新,避免并发刷新
  • refreshTokenRetryCount:记录重试次数
  • pendingRequests:401 请求队列,刷新完成后统一重试

2. 刷新 Token 函数(第62-141行)

流程:

  1. 从 localStorage 获取 refresh_token
  2. 调用 /api/auth/token,使用独立的 axios 实例(避免被拦截器拦截)
  3. 解析返回数据,支持两种格式:
    • 格式1:{ code: 200, data: { token, refreshToken } }
    • 格式2:直接返回 { token, refreshToken }
  4. 保存新 token 到 store 和 localStorage
  5. 如果有新的 refreshToken,也保存

3. 响应拦截器中的处理(第270-343行)

当接口返回 401 时:

  1. 检查是否为刷新 token 的请求本身(第272行)

    • 如果是,说明 refreshToken 已过期,直接跳转登录
  2. 检查是否为重试请求(第280行)

    • 如果 config._isRetry 为 true,说明是新 token 仍无效,跳转登录
  3. 检查重试次数(第287行)

    • 超过最大重试次数(2次),跳转登录
  4. 如果正在刷新(第294行)

    • 将请求加入 pendingRequests 队列,等待刷新完成
  5. 开始刷新 token(第304-325行)

    • 设置 isRefreshing = true
    • 调用 refreshToken() 获取新 token
    • 刷新成功后:
      • 处理队列中的请求(用新 token 重试)
      • 重新发起原始请求
    • 刷新失败后:
      • 拒绝队列中的所有请求
      • 跳转登录页

4. 关键设计点

  • 防止并发刷新:通过 isRefreshing 标志,确保同时只有一个刷新请求
  • 请求队列:401 请求先入队,刷新完成后统一重试
  • 重试标记:使用 _isRetry 标记重试请求,避免无限循环
  • 独立 axios 实例:刷新 token 的请求使用独立实例,避免被拦截器拦截

5. 错误处理

  • refreshToken 过期(401):跳转登录
  • 网络错误:保留原始错误信息
  • 数据格式错误:抛出明确的错误信息

整体流程:检测到 401 → 检查状态 → 刷新 token → 用新 token 重试请求 → 失败则跳转登录。

// axios配置  可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动
// The axios configuration can be changed according to the project, just change the file, other files can be left unchanged

import type { AxiosResponse, AxiosRequestConfig, AxiosInstance } from 'axios';
import { clone } from 'lodash-es';
import type { RequestOptions, Result } from '/#/axios';
import type { AxiosTransform, CreateAxiosOptions } from './axiosTransform';
import { VAxios } from './Axios';
import { checkStatus } from './checkStatus';
import { useGlobSetting } from '/@/hooks/setting';
import { useMessage } from '/@/hooks/web/useMessage';
import { RequestEnum, ContentTypeEnum } from '/@/enums/httpEnum';
import { isString } from '/@/utils/is';
import { getToken } from '/@/utils/auth';
import { setObjToUrlParams, deepMerge } from '/@/utils';
import { useErrorLogStoreWithOut } from '/@/store/modules/errorLog';
import { useI18n } from '/@/hooks/web/useI18n';
import { joinTimestamp, formatRequestDate } from './helper';
import axios from 'axios';
import { useUserStore } from '/@/store/modules/user';

// 扩展AxiosRequestConfig类型,添加_isRetry标记
declare module 'axios' {
  interface AxiosRequestConfig {
    _isRetry?: boolean;
  }
}

// 扩展Window类型,添加全局变量
declare global {
  interface Window {
    VITE_GLOB_API_URL?: string;
  }
}

const globSetting = useGlobSetting();
const urlPrefix = globSetting.urlPrefix;
const { createMessage, createErrorModal } = useMessage();

// token刷新相关状态管理
let isRefreshing = false; // 是否正在刷新token
let refreshTokenRetryCount = 0; // token刷新重试次数
const maxRefreshRetryCount = 2; // 最大重试次数
const pendingRequests: Array<{
  resolve: (value?: any) => void;
  reject: (reason?: any) => void;
  config: AxiosRequestConfig;
}> = []; // 等待token刷新完成的请求队列

/**
 * 重置刷新token相关状态
 */
function resetRefreshState(): void {
  isRefreshing = false;
  refreshTokenRetryCount = 0;
  pendingRequests.length = 0;
}

/**
 * 刷新token
 */
async function refreshToken(): Promise<string> {
  console.log('刷新token');

  const userStore = useUserStore();
  try {
    // 获取refresh token
    const refreshTokenValue = localStorage.getItem('refresh_token');

    if (!refreshTokenValue) {
      throw new Error('没有可用的refresh token');
    }

    // 调用刷新token的API(使用独立的axios实例,避免被拦截器拦截)
    const response = await axios.post(
      '/api/auth/token',
      {
        refreshToken: refreshTokenValue,
      },
      {
        baseURL: window.VITE_GLOB_API_URL,
        headers: {
          'Content-Type': 'application/json',
        },
      },
    );

    const { data } = response;

    console.log('刷新token返回数据:', data);

    // 根据后端返回格式调整
    // 后端可能直接返回 { token, refreshToken } 或者包装在 { code, data: { token, refreshToken } } 中
    let token = '';
    let refreshToken = '';

    if (data.code === 200 || data.success) {
      // 格式1: { code: 200, data: { token, refreshToken } }
      token = data.data?.token;
      refreshToken = data.data?.refreshToken;
    } else if (data.token) {
      // 格式2: 直接返回 { token, refreshToken }
      token = data.token;
      refreshToken = data.refreshToken;
    } else {
      throw new Error(data.message || '刷新token失败:返回数据格式不正确');
    }

    if (!token) {
      throw new Error('刷新token失败:未获取到token');
    }

    // 保存新token
    userStore.setToken(token);
    localStorage.setItem('jwt_token', token);

    // 如果有新的refresh token也一起保存
    if (refreshToken) {
      localStorage.setItem('refresh_token', refreshToken);
    }

    return token;
  } catch (error: any) {
    console.error('刷新token过程中出错:', error);

    // 如果refresh token接口返回401,说明refresh token已过期
    if (error?.response?.status === 401) {
      const errorMsg = error?.response?.data?.message || 'refresh token已过期';
      throw new Error(errorMsg);
    }

    // 如果是网络错误或其他错误,保留原始错误信息
    if (error?.message) {
      throw new Error(`刷新token失败: ${error.message}`);
    }

    // 其他错误统一抛出,但保留更多信息
    const errorMsg = error?.response?.data?.message || error?.message || '刷新token失败';
    throw new Error(errorMsg);
  }
}

/**
 * @description: 数据处理,方便区分多种处理方式
 */
const transform: AxiosTransform = {
  /**
   * @description: 处理响应数据。如果数据不是预期格式,可直接抛出错误
   */
  transformResponseHook: (res: AxiosResponse<Result>, options: RequestOptions) => {
    const { isTransformResponse, isReturnNativeResponse } = options;
    // 是否返回原生响应头 比如:需要获取响应头时使用该属性
    if (isReturnNativeResponse) {
      return res;
    }
    // 不进行任何处理,直接返回
    // 用于页面代码可能需要直接获取code,data,message这些信息时开启
    if (!isTransformResponse) {
      return res.data;
    }
    if (res) {
      return res.data;
    } else {
      return res;
    }
  },

  // 请求之前处理config
  beforeRequestHook: (config, options) => {
    //

    const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true, urlPrefix } = options;

    if (joinPrefix) {
      config.url = `${urlPrefix}${config.url}`;
    }

    if (apiUrl && isString(apiUrl)) {
      config.url = `${apiUrl}${config.url}`;
    }
    const params = config.params || {};
    const data = config.data || false;
    formatDate && data && !isString(data) && formatRequestDate(data);
    if (config.method?.toUpperCase() === RequestEnum.GET) {
      if (!isString(params)) {
        // 给 get 请求加上时间戳参数,避免从缓存中拿数据。
        config.params = Object.assign(params || {}, joinTimestamp(joinTime, false));
      } else {
        // 兼容restful风格
        config.url = config.url + params + `${joinTimestamp(joinTime, true)}`;
        config.params = undefined;
      }
    } else {
      if (!isString(params)) {
        formatDate && formatRequestDate(params);
        if (
          Reflect.has(config, 'data') &&
          config.data &&
          (Object.keys(config.data).length > 0 || config.data instanceof FormData)
        ) {
          config.data = data;
          config.params = params;
        } else {
          // 非GET请求如果没有提供data,则将params视为data
          config.data = params;
          config.params = undefined;
        }
        if (joinParamsToUrl) {
          config.url = setObjToUrlParams(
            config.url as string,
            Object.assign({}, config.params, config.data),
          );
        }
      } else {
        // 兼容restful风格
        config.url = config.url + params;
        config.params = undefined;
      }
    }
    return config;
  },

  /**
   * @description: 请求拦截器处理
   */
  requestInterceptors: (config, options) => {
    // 请求之前处理config
    const token = getToken();
    if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
      // jwt token
      (config as Recordable).headers['X-Authorization'] = options.authenticationScheme
        ? `${options.authenticationScheme} ${token}`
        : token;
    }
    return config;
  },

  /**
   * @description: 响应拦截器处理
   */
  responseInterceptors: (res: AxiosResponse<any>) => {
    return res;
  },

  /**
   * @description: 响应错误处理
   */
  responseInterceptorsCatch: async (axiosInstance: AxiosInstance, error: any) => {
    console.log('error+++++++++++++++++++++++++++++++++++', error.response?.data?.message);
    const { t } = useI18n();
    const errorLogStore = useErrorLogStoreWithOut();
    errorLogStore.addAjaxErrorInfo(error);
    const { response, code, message, config } = error || {};
    const errorMessageMode = config?.requestOptions?.errorMessageMode || 'none';
    const msg: string = response?.data?.message ?? '';
    const err: string = error?.toString?.() ?? '';
    let errMessage = '';
    const status = response?.status;

    if (axios.isCancel(error)) {
      return Promise.reject(error);
    }

    // 如果是特定的错误消息,不进行报错提示,直接返回
    if (msg === 'No Administration settings found for key: sms') {
      return Promise.reject(error);
    }

    // 如果是token过期(401状态码)
    if (status === 401) {
      // 检查是否是刷新token的请求本身返回401,如果是,直接跳转登录页
      if (config?.url?.includes('/api/auth/token') || config?.url?.includes('/auth/token')) {
        // 刷新token接口返回401,说明refresh token已过期,直接调用原逻辑跳转登录页
        resetRefreshState();
        checkStatus(status, msg, errorMessageMode);
        return Promise.reject(error);
      }

      // 如果这是刷新token后重新发起的请求,且还是返回401,说明新token也有问题,直接跳转登录页
      if (config?._isRetry) {
        resetRefreshState();
        checkStatus(status, msg, errorMessageMode);
        return Promise.reject(error);
      }

      // 如果刷新次数超过限制,直接调用原逻辑跳转到登录页
      if (refreshTokenRetryCount >= maxRefreshRetryCount) {
        resetRefreshState();
        checkStatus(status, msg, errorMessageMode);
        return Promise.reject(error);
      }

      // 如果正在刷新token,将请求加入队列等待
      if (isRefreshing) {
        return new Promise((resolve, reject) => {
          pendingRequests.push({
            resolve,
            reject,
            config,
          });
        });
      }

      // 开始刷新token
      isRefreshing = true;
      refreshTokenRetryCount++;

      try {
        // 调用刷新token逻辑
        await refreshToken();
        // 刷新成功,重置重试次数和刷新状态
        refreshTokenRetryCount = 0;
        isRefreshing = false;

        // 处理队列中的请求
        pendingRequests.forEach(({ resolve, config: pendingConfig }) => {
          // 标记这是刷新token后的请求
          pendingConfig._isRetry = true;
          resolve(axiosInstance(pendingConfig));
        });
        pendingRequests.length = 0;

        // 重新发起原始请求,标记这是刷新token后的请求
        config._isRetry = true;
        return axiosInstance(config);
      } catch (refreshError: any) {
        console.error('刷新token失败,错误详情:', refreshError);

        // 刷新失败,处理队列中的请求(全部拒绝)
        pendingRequests.forEach(({ reject }) => {
          reject(refreshError);
        });
        pendingRequests.length = 0;

        // 刷新失败或过期后,调用原逻辑跳转到登录页
        resetRefreshState();

        // 如果刷新token失败,使用刷新错误的信息,否则使用原始错误信息
        const finalMsg = refreshError?.message || msg || 'Token刷新失败,请重新登录';
        checkStatus(status, finalMsg, errorMessageMode);
        return Promise.reject(refreshError || error);
      }
    }

    if (status === 504) {
      createMessage.error('网关相应超时');
      return Promise.resolve(false);
    }

    // 处理其他错误
    try {
      if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
        errMessage = t('sys.api.apiTimeoutMessage');
      }
      if (err?.includes('Network Error')) {
        errMessage = t('sys.api.networkExceptionMsg');
      }

      if (errMessage) {
        if (errorMessageMode === 'modal') {
          createErrorModal({ title: t('sys.api.errorTip'), content: errMessage });
        } else if (errorMessageMode === 'message') {
          createMessage.error(errMessage);
        }
        return Promise.reject(error);
      }
    } catch (error) {
      throw new Error(error as unknown as string);
    }

    checkStatus(status, msg, errorMessageMode);
    return Promise.reject(error);
  },
};

function createAxios(opt?: Partial<CreateAxiosOptions>) {
  return new VAxios(
    // 深度合并
    deepMerge(
      {
        // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
        // authentication schemes,e.g: Bearer
        authenticationScheme: 'Bearer',
        // authenticationScheme: '',
        timeout: 10 * 1000,
        // 基础接口地址
        // baseURL: globSetting.apiUrl,

        headers: { 'Content-Type': ContentTypeEnum.JSON },
        // 如果是form-data格式
        // headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED },
        // 数据处理方式
        transform: clone(transform),
        // 配置项,下面的选项都可以在独立的接口请求中覆盖
        requestOptions: {
          // 默认将prefix 添加到url
          joinPrefix: true,
          // 是否返回原生响应头 比如:需要获取响应头时使用该属性
          isReturnNativeResponse: false,
          // 需要对返回数据进行处理
          isTransformResponse: true,
          // post请求的时候添加参数到url
          joinParamsToUrl: false,
          // 格式化提交参数时间
          formatDate: true,
          // 消息提示类型
          errorMessageMode: 'message',
          // 接口地址
          apiUrl: globSetting.apiUrl,
          // 接口拼接地址
          urlPrefix: urlPrefix,
          //  是否加入时间戳
          joinTime: false,
          // 忽略重复请求
          ignoreCancelToken: true,
          // 是否携带token
          withToken: true,
          retryRequest: {
            isOpenRetry: true,
            count: 5,
            waitTime: 100,
          },
        },
      },
      opt || {},
    ),
  );
}
export const defHttp = createAxios();

// other api url
// export const otherHttp = createAxios({
//   requestOptions: {
//     apiUrl: 'xxx',
//     urlPrefix: 'xxx',
//   },
// });

❌
❌