无感刷新网上能收到的例子做法很多,博主这里写这篇文章,一个是自己后期要用,方便直接抄代码,二是顺带加加自己网站的博客文章数,重新迁移后,以前网站的文档忘记备份,文章都被清空了

“无感刷新” 指的是在用户不感知的情况下,后台自动处理令牌刷新,并确保后续请求能够继续使用有效的令牌。为实现这种效果,通常会在令牌即将过期时提前刷新,以避免在实际请求时遇到 401 或其他认证失败的响应。

以下是无感刷新令牌的常见一般就两种实现思路:

  • 请求提前根据揭露的过期时间提前做令牌刷新

    • 优势:可以减少后端的请求次数,在第一个请求请求之前就可以判断是会否需要刷新令牌了,避免了不必要的网络开销,也能更好的的控制并发

    • 劣势:如果电脑时间改动,前后端没有约定好当前系统时间处理的话,就会导致每次请求都在这刷新令牌

  • 根据接口响应值做令牌刷新

    • 优势:不需要顾忌电脑时间的改动,也不需要去考虑过期时间,直接以接口响应的状态来判断是否需要刷新令牌

    • 劣势:刷新令牌依据接口响应,会多造成必要的网络开销,以及对大量请求的场景会造成接口响应积压的情况

由于两种方式,只是在处理令牌刷新的位置不同,其他需要注意的逻辑基本一样

比如:

  • 刷新后更新请求:在令牌刷新后,自动更新后续所有请求的令牌。

  • 并发请求的处理:如果多个请求同时触发令牌刷新,避免重复刷新。

以下axios举例,以博主自己常用的方式 根据接口响应值做令牌刷新 介绍大概的用法

安装

请挑选适合自己的安装方式

npm install axios --save

yarn add axios

pnpm install axios

导入,声明

import axios from 'axios';

// 创建一个 Axios 实例
const api = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 5000, // 请求超时时间
});

拦截器声明

博主个人习惯请求拦截和响应拦截单独写一个文件,看官们请根据自己喜好,爱咋搞就咋搞

创建request_interceptors.jsresponse_interceptors.js 文件,并编写代码

request_interceptors.js

这个部分比较简单

  1. 为所有请求赋值请求令牌

  2. 为所有请求赋值请求时间

import { stores } from '@/stores';

export default {
  success: function (config) {
    config.headers['X-Real-Request'] = new Date().getTime();
    if (stores.authorize.isLogin) {
      config.headers['Authorization'] = `Bearer ${stores.authorize.token}`;
    }

    return config;
  },
  error: function (error) {
    console.log(error); // for debug
    return Promise.reject(error);
  }
};

response_interceptors.js

1. 导入store,并声明两个变量来方便我们控制并发处理
import { stores } from '@/stores';

// 标记是否正在刷新令牌
let isRefreshing = false; 
// 用于存储等待刷新的请求
let refreshSubscribers = [];
2. 创建刷新令牌的api方法
/**
 * 令牌刷新api
 * @param {*} axios
 * @returns
 */
const tokenRefreshApi = function (axios) {
  return axios({
    method: 'post',
    url: '*/refreshtoken',
    data: {
      refreshToken: stores.authorize.refreshToken
    },
    timeout: 60000
  });
};
3. 声明拦截方法

由于博主的api并非使用状态码401的形式,所以此处以拦截器的success方法为例

首先声明success方法

export default {
  success: async function (resp, axios) {
  }
}
4. 检验返回值

博主这里的接口响应值格式是:{ "resCode": 0, "resMsg":"" } 所以以这个格式来举例

    // 判断api基础响应格式
    if (resp.data && resp.data.resCode >= 0) {
      return Promise.resolve(resp);
    }
    // 赋值默认错误返回格式
    const res = {
      resCode: -99,
      resMsg: ''
    };

    // 响应格式校验
    if (!resp.data) {
      res.resMsg = '系统异常,请稍候再试';
    } else if (resp.data.resCode == undefined || resp.data.resCode == null) {
      res.resMsg = '响应异常';
    } else if (resp.data.resCode === -99) {
      res.resMsg = resp.data.resMsg || '操作失败';
    }
5. 校验令牌过期状态,并设置并发控制

博主这边令牌过期返回状态码为-201

if (resp.data.resCode == -201){
  // 首先将原有的请求赋值给变量
  const originalRequest = { ...resp.config };

  // 判断当前是否处于在刷新令牌情况下
  if (!isRefreshing) {
    // 将状态置为正在刷新令牌
    isRefreshing = true;
  } else {
    // 已经在刷新令牌,将原始请求加入等待队列
    return new Promise(resolve => {
      refreshSubscribers.push(newToken => {
        originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
        resolve(axios(originalRequest));
      });
    });
  }
}
6. 令牌刷新
if (!isRefreshing) {
  isRefreshing = true;
  // 刷新令牌
  if (!stores.authorize.data.refreshToken) {
    // 跳转登录页
    return Promise.reject({
      status: 200,
      data: {
        ...res.data
      }
    });
  }

  const resp = await tokenRefreshApi(axios);
  if (!resp.data) {
    // 跳转登录页
    return Promise.reject({
      status: 200,
      data: {
        ...res.data
      }
    });
  }

  const res = resp.data;
  if (!res || res.resCode != 0) {
    // 令牌刷新跳转登录页
    return Promise.reject({
      status: 200,
      data: {
        ...res.data
      }
    });
  }

  stores.authorize.login(res.data);

  originalRequest.headers['Authorization'] = `Bearer ${res.data.token}`;

  while (refreshSubscribers && refreshSubscribers.length) {
    const req = refreshSubscribers.shift();
    req(stores.authorize.data.token);
  }

  isRefreshing = false;

  return axios(originalRequest);
} else {
  // 已经在刷新令牌,将原始请求加入等待队列
  return new Promise(resolve => {
    refreshSubscribers.push(newToken => {
      originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
      resolve(axios(originalRequest));
    });
  });
}

主要逻辑

其实无感刷新的主要注意点是在于并发控制的问题:

  1. 同时多个请求触发,该如何有效的将第一个请求结果拦截,并无感刷新令牌后,重新请求

  2. 在刷新令牌期间后续得请求进来该如何拦截,并进额外处理


完整代码

import { stores } from '@/stores';

// 标记是否正在刷新令牌
let isRefreshing = false;
// 用于存储等待刷新的请求
let refreshSubscribers = [];

/**
 * 令牌刷新api
 * @param {*} axios
 * @returns
 */
const tokenRefreshApi = function (axios) {
  return axios({
    method: 'post',
    url: '*/refreshtoken',
    data: {
      refreshToken: stores.authorize.refreshToken
    },
    timeout: 60000
  });
};

export default {
  success: async function (resp, axios) {
    // const router = useRouter();

    if (resp.data && resp.data.resCode >= 0) {
      return Promise.resolve(resp);
    }

    const res = {
      resCode: -99,
      resMsg: ''
    };

    if (!resp.data) {
      res.resMsg = '系统异常,请稍候再试';
    } else if (resp.data.resCode == undefined || resp.data.resCode == null) {
      res.resMsg = '响应异常';
    } else if (resp.data.resCode === -99) {
      res.resMsg = resp.data.resMsg || '操作失败';
    }

    if (resp.data.resCode == -21) {
      const originalRequest = { ...resp.config };
      if (!isRefreshing) {
        isRefreshing = true;
        // 刷新令牌
        if (!stores.authorize.data.refreshToken) {
          // 跳转登录页
          return Promise.reject({
            status: 200,
            data: {
              ...res.data
            }
          });
        }

        const resp = await tokenRefreshApi(axios);
        if (!resp.data) {
          // 跳转登录页
          return Promise.reject({
            status: 200,
            data: {
              ...res.data
            }
          });
        }

        const res = resp.data;
        if (!res || res.resCode != 0) {
          // 跳转登录页
          return Promise.reject({
            status: 200,
            data: {
              ...res.data
            }
          });
        }

        stores.authorize.login(res.data);

        originalRequest.headers['Authorization'] = `Bearer ${res.data.token}`;

        while (refreshSubscribers && refreshSubscribers.length) {
          const req = refreshSubscribers.shift();
          req(stores.authorize.token);
        }

        isRefreshing = false;

        return axios(originalRequest);
      } else {
        // 已经在刷新令牌,将原始请求加入等待队列
        return new Promise(resolve => {
          refreshSubscribers.push(newToken => {
            originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
            resolve(axios(originalRequest));
          });
        });
      }
    }

    resp.data = res;
    return Promise.resolve(resp);
  },
  error: function (err) {
    return Promise.reject({
      status: -999,
      msg: err.message
    });
  }
};