【axios】不可不知的axios无感刷新token

3,283 阅读2分钟

背景

最近项目用户登录页面处于安全考虑,登录之后的access_token并非永久有效而是有实效性了,因此 需要在用户登录之后token失效的情况下进行access_token的无感知刷新,也既是需要前端在access_token失效时调用一次refresh API使得access_token再次有效,而不影响用户的使用体验。

需求拆解

用户登录成功之后API会将{ access_token: '', refresh_token: '', expired: '' }返回,那就可以在用户发起一个请求时,判断access_token是否过期,过期就刷新access_token。首先判断access_token过期可以在请求之前根据expired判断,也可以在请求结果回来之后判断,问题的难点其实是指如何在多个请求进来时将这些请求暂存下来,然后在refresh之后在做请求场景还原。

实现思路

针对需求拆解主要需要解决两个问题,一是失效判断,二是多个请求还原,具体解法如下:

  1. 失效判断:(1)请求前根据expired的过期时间进行判断,但这种操作是不可靠的,因为时间是可修改的,所以不采用该方法;(2)根据请求结果判断access_token过期,然后进行刷新操作,然后在进行一次请求。
  2. 多请求的场景还原:当同时有多个过期请求进来时,需要避免多次refresh的请求,设置一个标志位,当已经有refresh的操作在进行时,再进来的请求进行挂起(此时可以利用Promisepending状态做文章),等待refresh成功之后在进行请求。

伪代码实现如下

import axios, { AxiosRequestConfig } from 'axios';

interface IRequestConfig {
   tokenInfoKey: string; // 由于登录之后的 token 数据时存在 localstorage 需要键值
   onRefreshError: VoidFunction;
}

interface ITokenInfo {
    access_token: string;
    refresh_token: string;
    expired: string;
}

class Request {
  private config: IRequestConfig = {};
  private tokenInfo: ITokenInfo | null = null;
  private isRefreshing: boolean = false;
  private needRetryRequest = [];
  constructor (config: IRequestConfig) {
     const { tokenInfoKey } = config;
     this.tokenInfo = JSON.parse(localStorage.getItem(tokenInfoKey));
     this.config = config;
  }
  
  refreshAccessToken (onSuccess: VoidFunction) {
      return axios.request<ITokenInfo>({
          url: '',
          method: '',
      })
          .then((res) => {
              localStorage.setItem(this.config.tokenInfoKey, res.data);
              onSuccess();
          })
          .catch(() => {
              this.needRetryRequest = [];
          });
  }
  
  request (config: AxiosRequestConfig) {
     if (this.isRefreshing) {
         return new Promise((resolve) => {
             this.needRetryRequest.push(async () => await resolve(axios));
         });
     }
     return axios
       .request(config)
       .then(res => {
           return res.data;
       })
       .catch(e => {
           // 假设 401401 定义需要 refresh
           const code = 401401;
           const { status } = e.response;
           if (status === code) {
               this.isRefreshing = true;
               this.refreshAccessToken(() => {
                   Promise.all([() => this.request(config), ...this.needRetryRequest].map(cb => cb()));
               });
           }
       })
       .finally(() => {
           this.isRefreshing = false;
       });
  }
}