Skip to content

axios 封装实践

在封装 axios 之前,先了解一下 axios 特性和功能,了解为什么需要封装 axios。

axios 功能特性

axios 官网:起步 | Axios中文文档 | Axios中文网 (axios-http.cn)

Axios 是一个基于 promise 网络请求库,作用于node.js 和浏览器中。 它是 isomorphic 的(即同一套代码可以运行在浏览器和node.js中)。在服务端它使用原生 node.js http 模块, 而在客户端 (浏览端) 则使用 XMLHttpRequests。

特性

  • 从浏览器创建 XMLHttpRequests
  • 从 node.js 创建 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求和响应数据
  • 取消请求
  • 超时处理
  • 查询参数序列化支持嵌套项处理
  • 自动将请求体序列化为:
    • JSON (application/json)
    • Multipart / FormData (multipart/form-data)
    • URL encoded form (application/x-www-form-urlencoded)
  • 将 HTML Form 转换成 JSON 进行请求
  • 自动转换JSON数据
  • 获取浏览器和 node.js 的请求进度,并提供额外的信息(速度、剩余时间)
  • 为 node.js 设置带宽限制
  • 兼容符合规范的 FormData 和 Blob(包括 node.js)
  • 客户端支持防御XSRF

为什么要封装 axios?

在我的实践中发现,当 axios 应用于一些前端项目中,如后台管理系统中,需要 axios 以上介绍的基本功能之外,常常需要加入 防止接口重复请求、消息提示、超时重试、状态码检查处理,特别是后台管理系统中,大部分还需要:请求携带 token、刷新 token、token 校验、token 刷新、错误退出登录等功能。

在不同台管理系统中,需要反复对 axios 封装以上功能。

因此,为了提高开发效率,我们可以提前将以上功能封装成一个工具库并发布到 npm,通过配置来增加以上功能,减少重复封装操作,提高开发效率。

新增 axios 功能

功能介绍
消息提示成功/失败/错误等消息和模态弹窗提示
在浏览器端使用消息和模态弹窗提示
在node 端使用 console 打印
忽略重复请求
超时重试
加入请求时间戳请求时间戳可以用于调试接口
上传文件默认采用 formdata 格式上传文件默认配置:
header:{Content-type: FORM_DATA = 'multipart/form-data;charset=UTF-8',}
固定状态码提示和处理
ajax 错误日志收集钩子addAjaxErrorInfo
全局请求 token
token 校验
token 刷新
错误清除 token
错误退出登录
提供请求处理钩子
请求之前处理配置: beforeRequestHook
请求成功处理: transformRequestData
请求失败处理: requestCatch
请求之前的拦截器: requestInterceptors
请求之后的拦截器: responseInterceptors
请求之前的拦截器错误处理: requestInterceptorsCatch
请求之后的拦截器错误处理: responseInterceptorsCatch

配置

axios 请求配置

这些是创建请求时可以用的配置选项。只有 url 是必需的。如果没有指定 method,请求将默认使用 GET 方法。

{
  // `url` 是用于请求的服务器 URL
  url: '/user',

  // `method` 是创建请求时使用的方法
  method: 'get', // 默认值

  // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
  // 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
  baseURL: 'https://some-domain.com/api/',

  // `transformRequest` 允许在向服务器发送前,修改请求数据
  // 它只能用于 'PUT', 'POST' 和 'PATCH' 这几个请求方法
  // 数组中最后一个函数必须返回一个字符串, 一个Buffer实例,ArrayBuffer,FormData,或 Stream
  // 你可以修改请求头。
  transformRequest: [function (data, headers) {
    // 对发送的 data 进行任意转换处理

    return data;
  }],

  // `transformResponse` 在传递给 then/catch 前,允许修改响应数据
  transformResponse: [function (data) {
    // 对接收的 data 进行任意转换处理

    return data;
  }],

  // 自定义请求头
  headers: {'X-Requested-With': 'XMLHttpRequest'},

  // `params` 是与请求一起发送的 URL 参数
  // 必须是一个简单对象或 URLSearchParams 对象
  params: {
    ID: 12345
  },

  // `paramsSerializer`是可选方法,主要用于序列化`params`
  // (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
  paramsSerializer: function (params) {
    return Qs.stringify(params, {arrayFormat: 'brackets'})
  },

  // `data` 是作为请求体被发送的数据
  // 仅适用 'PUT', 'POST', 'DELETE 和 'PATCH' 请求方法
  // 在没有设置 `transformRequest` 时,则必须是以下类型之一:
  // - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
  // - 浏览器专属: FormData, File, Blob
  // - Node 专属: Stream, Buffer
  data: {
    firstName: 'Fred'
  },
  
  // 发送请求体数据的可选语法
  // 请求方式 post
  // 只有 value 会被发送,key 则不会
  data: 'Country=Brasil&City=Belo Horizonte',

  // `timeout` 指定请求超时的毫秒数。
  // 如果请求时间超过 `timeout` 的值,则请求会被中断
  timeout: 1000, // 默认值是 `0` (永不超时)

  // `withCredentials` 表示跨域请求时是否需要使用凭证
  withCredentials: false, // default

  // `adapter` 允许自定义处理请求,这使测试更加容易。
  // 返回一个 promise 并提供一个有效的响应 (参见 lib/adapters/README.md)。
  adapter: function (config) {
    /* ... */
  },

  // `auth` HTTP Basic Auth
  auth: {
    username: 'janedoe',
    password: 's00pers3cret'
  },

  // `responseType` 表示浏览器将要响应的数据类型
  // 选项包括: 'arraybuffer', 'document', 'json', 'text', 'stream'
  // 浏览器专属:'blob'
  responseType: 'json', // 默认值

  // `responseEncoding` 表示用于解码响应的编码 (Node.js 专属)
  // 注意:忽略 `responseType` 的值为 'stream',或者是客户端请求
  // Note: Ignored for `responseType` of 'stream' or client-side requests
  responseEncoding: 'utf8', // 默认值

  // `xsrfCookieName` 是 xsrf token 的值,被用作 cookie 的名称
  xsrfCookieName: 'XSRF-TOKEN', // 默认值

  // `xsrfHeaderName` 是带有 xsrf token 值的http 请求头名称
  xsrfHeaderName: 'X-XSRF-TOKEN', // 默认值

  // `onUploadProgress` 允许为上传处理进度事件
  // 浏览器专属
  onUploadProgress: function (progressEvent) {
    // 处理原生进度事件
  },

  // `onDownloadProgress` 允许为下载处理进度事件
  // 浏览器专属
  onDownloadProgress: function (progressEvent) {
    // 处理原生进度事件
  },

  // `maxContentLength` 定义了node.js中允许的HTTP响应内容的最大字节数
  maxContentLength: 2000,

  // `maxBodyLength`(仅Node)定义允许的http请求内容的最大字节数
  maxBodyLength: 2000,

  // `validateStatus` 定义了对于给定的 HTTP状态码是 resolve 还是 reject promise。
  // 如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),
  // 则promise 将会 resolved,否则是 rejected。
  validateStatus: function (status) {
    return status >= 200 && status < 300; // 默认值
  },

  // `maxRedirects` 定义了在node.js中要遵循的最大重定向数。
  // 如果设置为0,则不会进行重定向
  maxRedirects: 5, // 默认值

  // `socketPath` 定义了在node.js中使用的UNIX套接字。
  // e.g. '/var/run/docker.sock' 发送请求到 docker 守护进程。
  // 只能指定 `socketPath` 或 `proxy` 。
  // 若都指定,这使用 `socketPath` 。
  socketPath: null, // default

  // `httpAgent` and `httpsAgent` define a custom agent to be used when performing http
  // and https requests, respectively, in node.js. This allows options to be added like
  // `keepAlive` that are not enabled by default.
  httpAgent: new http.Agent({ keepAlive: true }),
  httpsAgent: new https.Agent({ keepAlive: true }),

  // `proxy` 定义了代理服务器的主机名,端口和协议。
  // 您可以使用常规的`http_proxy` 和 `https_proxy` 环境变量。
  // 使用 `false` 可以禁用代理功能,同时环境变量也会被忽略。
  // `auth`表示应使用HTTP Basic auth连接到代理,并且提供凭据。
  // 这将设置一个 `Proxy-Authorization` 请求头,它会覆盖 `headers` 中已存在的自定义 `Proxy-Authorization` 请求头。
  // 如果代理服务器使用 HTTPS,则必须设置 protocol 为`https`
  proxy: {
    protocol: 'https',
    host: '127.0.0.1',
    port: 9000,
    auth: {
      username: 'mikeymike',
      password: 'rapunz3l'
    }
  },

  // see https://axios-http.com/zh/docs/cancellation
  cancelToken: new CancelToken(function (cancel) {
  }),

  // `decompress` indicates whether or not the response body should be decompressed 
  // automatically. If set to `true` will also remove the 'content-encoding' header 
  // from the responses objects of all decompressed responses
  // - Node only (XHR cannot turn off decompression)
  decompress: true // 默认值

}

封装的 axios 默认配置

xhttp 除了支持 axios 默认请求配置外,新增了以下参数配置,以上 axios 配置添加 requestOptions 参数中

const axios = new VAxios({
  // 认证方案,例如: Bearer
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
  authenticationScheme: '',
  // 接口超时时间 单位毫秒
  timeout: 10 * 1000,
  // 接口可能会有通用的地址部分,可以统一抽取出来
  prefixUrl: prefix,
  headers: { 'Content-Type': ContentTypeEnum.JSON },
  // 数据处理方式,见下方说明
  transform,
  // 配置项,下面的选项都可以在独立的接口请求中覆盖
  requestOptions: {
    // 默认将prefix 添加到url
    joinPrefix: true,
    // 是否返回原生响应头 比如:需要获取响应头时使用该属性
    isReturnNativeResponse: false,
    // 需要对返回数据进行处理
    isTransformRequestResult: true,
    // post请求的时候添加参数到url
    joinParamsToUrl: false,
    // 格式化提交参数时间
    formatDate: true,
    // 消息提示类型
    errorMessageMode: 'message',
    // 接口地址,如:要所有接口添加 '/api' 前缀,则 apiUrl 为 '/api'
    apiUrl: globSetting.apiUrl,
    //  是否加入时间戳
    joinTime: true,
    // 忽略重复请求
    ignoreCancelToken: true,
  },
});

transform 数据处理说明

类型定义,见 axiosTransform.ts 文件

export abstract class AxiosTransform {
  /**
   * @description: 请求之前处理配置
   */
  beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;

  /**
   * @description: 请求成功处理
   */
  transformRequestData?: (res: AxiosResponse<Result>, options: RequestOptions) => any;

  /**
   * @description: 请求失败处理
   */
  requestCatch?: (e: Error) => Promise<any>;

  /**
   * @description: 请求之前的拦截器
   */
  requestInterceptors?: (config: AxiosRequestConfig) => AxiosRequestConfig;

  /**
   * @description: 请求之后的拦截器
   */
  responseInterceptors?: (res: AxiosResponse<any>) => AxiosResponse<any>;

  /**
   * @description: 请求之前的拦截器错误处理
   */
  requestInterceptorsCatch?: (error: Error) => void;

  /**
   * @description: 请求之后的拦截器错误处理
   */
  responseInterceptorsCatch?: (error: Error) => void;
}

项目默认 transform 处理逻辑,可以根据各自项目进行处理。一般需要更改的部分为下方代码,见代码注释说明

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

    const { data } = res;
    if (!data) {
      // return '[HTTP] Request has no return value';
      throw new Error(t('sys.api.apiRequestFailed'));
    }
    //  这里 code,result,message为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
    const { code, result, message } = data;

    // 这里逻辑可以根据项目进行修改
    const hasSuccess = data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS;
    if (hasSuccess) {
      return result;
    }

    // 在此处根据自己项目的实际情况对不同的code执行不同的操作
    // 如果不希望中断当前请求,请return数据,否则直接抛出异常即可
    let timeoutMsg = '';
    switch (code) {
      case ResultEnum.TIMEOUT:
        timeoutMsg = t('sys.api.timeoutMessage');
      default:
        if (message) {
          timeoutMsg = message;
        }
    }

    // errorMessageMode=‘modal’的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误
    // errorMessageMode='none' 一般是调用时明确表示不希望自动弹出错误提示
    if (options.errorMessageMode === 'modal') {
      createErrorModal({ title: t('sys.api.errorTip'), content: timeoutMsg });
    } else if (options.errorMessageMode === 'message') {
      createMessage.error(timeoutMsg);
    }

    throw new Error(timeoutMsg || t('sys.api.apiRequestFailed'));
  },

  // 请求之前处理config
  beforeRequestHook: (config, options) => {
    const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true } = options;

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

    if (apiUrl && isString(apiUrl)) {
      config.url = `${apiUrl}${config.url}`;
    }
    const params = config.params || {};
    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);
        config.data = params;
        config.params = undefined;
        if (joinParamsToUrl) {
          config.url = setObjToUrlParams(config.url as string, config.data);
        }
      } else {
        // 兼容restful风格
        config.url = config.url + params;
        config.params = undefined;
      }
    }
    return config;
  },

  /**
   * @description: 请求拦截器处理
   */
  requestInterceptors: (config, options) => {
    // 请求之前处理config
    const token = getToken();
    if (token) {
      // jwt token
      config.headers.Authorization = options.authenticationScheme
        ? `${options.authenticationScheme} ${token}`
        : token;
    }
    return config;
  },

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

  /**
   * @description: 响应错误处理
   */
  responseInterceptorsCatch: (error: any) => {
    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?.error?.message ?? '';
    const err: string = error?.toString?.() ?? '';
    let errMessage = '';

    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);
    }

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

更改参数格式

项目接口默认为 Json 参数格式,即 headers: { 'Content-Type': ContentTypeEnum.JSON },

如果需要更改为 form-data 格式,更改 headers 的 'Content-TypeContentTypeEnum.FORM_URLENCODED 即可

多个接口地址

当项目中需要用到多个接口地址时, 可以在 src/utils/http/axios/index.ts 导出多个 axios 实例

// 目前只导出一个默认实例,接口地址对应的是环境变量中的 VITE_GLOB_API_URL 接口地址
export const defHttp = createAxios();

// 需要有其他接口地址的可以在后面添加

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

删除请求 URL 携带的时间戳参数

如果不需要 url 上面默认携带的时间戳参数 ?_t=xxx

const axios = new VAxios({
  requestOptions: {
    // 是否加入时间戳
    joinTime: false,
  },
});

封装后新增的配置项

axios响应结构

一个请求的响应包含以下信息。

{
  // `data` 由服务器提供的响应
  data: {},

  // `status` 来自服务器响应的 HTTP 状态码
  status: 200,

  // `statusText` 来自服务器响应的 HTTP 状态信息
  statusText: 'OK',

  // `headers` 是服务器响应头
  // 所有的 header 名称都是小写,而且可以使用方括号语法访问
  // 例如: `response.headers['content-type']`
  headers: {},

  // `config` 是 `axios` 请求的配置信息
  config: {},

  // `request` 是生成此响应的请求
  // 在node.js中它是最后一个ClientRequest实例 (in redirects),
  // 在浏览器中则是 XMLHttpRequest 实例
  request: {}
}

当使用 then 时,您将接收如下响应:

axios.get('/user/12345')
  .then(function (response) {
    console.log(response.data);
    console.log(response.status);
    console.log(response.statusText);
    console.log(response.headers);
    console.log(response.config);
  });

当使用 catch,或者传递一个rejection callback作为 then 的第二个参数时,响应可以通过 error 对象被使用,正如在错误处理部分解释的那样。

封装后 axios 响应结构

默认服务端返回默认为以下数据格式:

{
    'code': 0, // 0 表示成功,-1 表示失败
    'message': '返回消息',
    'result': {} // 响应结构
    }
}

如果服务端返回接口无法返回以上固定格式,则需要在 createXhttp 第一个参数传入 formatResponse 方法,对返回的数据格式进行格式化。

如服务端返回如下格式:

{
    "Code": 200, // 状态码
    "Msg": "OK", // 提示信息
    "Data": {} // 数据
}

则在创建 http 实例时,配置如下:

import { createXhttp } from 'sewen-ui/xhttp'

export default createXhttp({
    tokenKey: 'test-token', // token key值,传入token key值,默认使用内部获取方法
    tokenExpires: 1, // token 过期时间
    formatResponse: (data)=> {//参数 formatResponse 方法,对返回的数据格式进行格式化。
        return { // 将服务端返回的数据格式化
            code: data.Code,
            message: data.Msg,
            result: data.Data
        }
    } , // 存储token 方法
},
{
    requestOptions: {
        urlPrefix: 'api'
    }
});

Token 刷新实现

前端无感刷新 token 常见方案:

方案描述优点缺点
请求拦截刷新后端返回过期时间,前端每次请求判断token过期时间,快过期时调用刷新token接口实时性较高,能较准确地判断token是否即将过期若本地时间被篡改,特别是比服务器时间慢时,拦截会失败
定时刷新后端返回过期时间,前端使用定时器定时刷新token接口无需每次请求都判断token过期时间浪费资源,消耗性能
响应拦截刷新在请求响应拦截器中拦截,判断token过期后调用刷新token接口无需额外判断或定时器,自动处理token过期需要后端支持特定的错误码(如401)来触发刷新
双token机制双token机制(Access Token与Refresh Token)较高的安全性和用户体验,减少用户频繁登录需要后端支持双token的发放和刷新机制

请求拦截刷新 Token

该刷新token方案主要依赖于前端框架(如Axios)的拦截器功能,结合后端返回的状态码来判断token是否过期,并自动进行token的刷新操作。

实现思路

  1. 后端生成Token并返回过期时间
    • 当用户首次登录或需要获取新的Token时,后端验证用户身份并生成一个AccessToken(通常带有过期时间)。
    • 除了AccessToken,后端还应返回一个表示Token过期时间的字段(如expires_in),这个字段表示从当前时间开始,AccessToken还有多少秒的有效期。
  2. 前端存储Token和过期时间
    • 前端接收到AccessToken和过期时间后,将它们存储在本地(如localStorage、sessionStorage或Vuex等)。
  3. 前端每次请求时判断Token是否快过期
    • 在发送每个需要身份验证的请求之前,前端从本地存储中获取AccessToken和过期时间。
    • 计算当前时间与Token过期时间的差值,如果差值小于某个阈值(如剩余时间的1/3或1/2),则认为Token快过期了。
  4. 调用刷新Token接口
    • 如果Token快过期了,前端调用后端提供的刷新Token接口,并发送旧的AccessToken(或其他认证信息)作为参数。
    • 后端验证旧的AccessToken仍然有效后,生成一个新的AccessToken和新的过期时间,并返回给前端。
  5. 前端更新Token
    • 前端接收到新的AccessToken和过期时间后,更新本地存储中的Token信息。
    • 然后使用新的AccessToken继续发送原来的请求。

注意事项

  1. 并发请求问题:
    • 如果在短时间内有多个请求同时被拦截并触发token刷新操作,可能会导致多个刷新token的请求被同时发送。
    • 为了避免这种情况,可以使用锁机制或队列机制来确保同一时间只有一个刷新token的请求被发送。
  2. 时间同步问题:
    • 如果客户端(前端)和服务器的时间不一致,尤其是客户端时间比服务器时间慢时,可能会出现Token已经过期但前端认为还未过期的情况。
    • 因此,建议服务器返回的时间戳使用UTC时间,并确保客户端和服务器的时间尽量同步。
  3. Token泄露风险:
    • 由于前端需要存储Token,因此存在Token被泄露的风险。
    • 要确保Token的存储和传输都是安全的(如使用HTTPS和加密存储)。
  4. 刷新Token频率:
    • 如果前端过于频繁地调用刷新Token接口,可能会给后端带来不必要的压力。
    • 因此,需要合理设置Token的过期时间和快过期的阈值,确保在不过度增加后端压力的同时,也能保证用户体验。
  5. 处理Token刷新失败的情况:
    • 如果因为某些原因(如网络问题、Token已被其他设备使用导致失效等)导致刷新Token失败,前端需要有相应的处理逻辑,如提示用户重新登录或跳转到登录页面等。

响应拦截刷新 Token

该刷新token方案主要依赖于前端框架(如Axios)的拦截器功能,结合后端返回的状态码来判断token是否过期,并自动进行token的刷新操作。

实现思路

  1. 设置请求拦截器:
    • 在前端发起请求之前,通过请求拦截器获取存储在本地(如localStorage、sessionStorage或Vuex等)的token,并将其附加到请求的头部(如Authorization: Bearer <token>)。
  2. 设置响应拦截器:
    • 在前端收到服务器响应后,通过响应拦截器判断响应的状态码。
    • 如果状态码为401(通常表示未授权或token已过期),则执行token刷新的逻辑。
  3. 判断token过期并刷新:
    • 当响应拦截器判断token过期时,前端发起一个刷新token的请求(通常携带旧的token或其他认证信息),以从后端获取新的token。
    • 后端验证旧的token仍然有效后,生成一个新的token并返回给前端。
  4. 处理刷新token的响应:
    • 前端收到刷新token的响应后,将新的token存储在本地,并重新发起之前被拦截的请求(此时请求将携带新的token)。
    • 需要注意的是,为了避免重复发送请求(即刷新token成功后再次发送被拦截的请求),可以将需要重试的请求暂时存储起来,并在获取到新token后依次重试。
  5. 处理token刷新失败的情况:
    • 如果刷新token的请求失败(如网络问题、旧token已失效等),前端需要有相应的处理逻辑,如跳转到登录页面或提示用户重新登录。

注意事项

  1. 循环请求问题:
    • 当token过期且刷新token的请求也失败时,如果前端没有合适的处理逻辑,可能会陷入循环请求的状态。
    • 为了避免这种情况,可以设置一个重试次数限制,或者在重试之前增加一定的延时。
  2. 并发请求问题:
    • 如果在短时间内有多个请求同时被拦截并触发token刷新操作,可能会导致多个刷新token的请求被同时发送。
    • 为了避免这种情况,可以使用锁机制或队列机制来确保同一时间只有一个刷新token的请求被发送。
  3. 安全性问题:
    • 在整个过程中,要确保token的存储和传输都是安全的(如使用HTTPS和加密存储)。
    • 同时,要注意不要在前端暴露敏感信息(如密码、密钥等),以防止被恶意攻击者利用。
  4. 后端支持:
    • 该方案需要后端支持在token过期时返回特定的状态码(如401),并提供刷新token的接口。
    • 因此,前端和后端需要紧密配合,确保接口的正确实现和调用。
  5. 用户体验问题:
    • 在token刷新过程中,用户可能会感受到一定的延迟或中断。
    • 为了提高用户体验,可以在合适的地方添加提示信息或动画效果来告知用户正在刷新token。
    • 同时,要尽量减少刷新token的频率和对用户体验的影响。

贡献者

sewen-ui design by Sewen