前端,你还不会刷新token吗?

647 阅读2分钟

一、什么情况下需要做token刷新?

背景:单点登录使用token方案

目的:减轻服务器压力

双token的好处:具有权限控制的能力

在单点登录情况下,使用token做权限验证,可以减轻服务器的压力。使用两个token,accessToken和refreshToken。当使用accessToken访问 其他服务过期时,需要去CAS 服务刷新token,然后再去请求业务服务器。

控制refreshToken,就可以达到踢人下线等目的。

CAS (Central Authentication Service)中心授权服务

单点登录(SSO:Single Sign On

用户只需要登录一次,就可以在多个系统中进行访问。类似于在百度系网站中,你在百度网页登录了百度账号,旗下的贴吧,网盘等不需要再次登录,就可以使用。

二、如何实现token刷新?

画板

优化:使用队列优化未发出的请求

当已经出现未授权错误时,之后进入的请求都放入到一个队列中,当刷新token成功后,修改token,再发出,达到减轻服务器压力的目的

import { isAxiosError, type AxiosError, type AxiosResponse } from 'axios';
import type { FReqConfig, ReqItem, SetupRefreshTokenOptions } from './type';

export function useRefreshToken({
  refreshToken,
  unauthorizedErrorHandler,
  http,
  setToken,
  getIsUnauthorized
}: SetupRefreshTokenOptions) {
  
  let promise: Promise<any> | null = null;
  let isRefreshToken = false;

  /**
   * ## 收集401失败请求,如果没有正在刷新token,则刷新
   * @param resp
   */
  async function handleUnauthorized(resp: AxiosResponse | AxiosError) {

    const { status, config } = resp || {};
    let isUnauthorized = false;
    const isError = await isAxiosError(resp);
    
    if (isError) {
      isUnauthorized = status === 401;
    } else {
      if (typeof getIsUnauthorized == 'function') {
        isUnauthorized = getIsUnauthorized(resp);
      }
    }

    // 如果当前正在刷新中,则返回promise
    if (isUnauthorized) {
      if (!isRefreshToken) _refreshToken();
      
      const isSuccess = await promise;
      if (isSuccess) {
        return http.request(setToken(config!));
      } else {
        if (typeof unauthorizedErrorHandler === 'function') unauthorizedErrorHandler();

        return Promise.reject(resp);
      }
    }

    // 不是未授权错误
    if (isError) {
      return Promise.reject(resp);
    } else {
      return Promise.resolve(resp);
    }
  }

  /** ## 收集 401 后发起的请求,等token刷新后再执行 */
  let queue: ReqItem[] = [];

  /**
   * # 当刷新token时,收集将要执行的请求
   * @param config
   * @returns axiosConfig
   */
  function handleReqWhenUnauthorized(config: FReqConfig): Promise<FReqConfig> {
    if (isRefreshToken) {
      return new Promise((resolve) => {
        queue.push({ resolve, config, type: 'request' });
      });
    }

    return Promise.resolve(config);
  }

  /**
   * ## 处理刷新token的逻辑
   * 1. 所有的401 请求都使用一个 刷新token的 promise
   * 2. 刷新token成功后,重新执行因为401 而没有发起的请求
   */
  async function _refreshToken() {
    isRefreshToken = true;
    if (!promise) {
      promise = refreshToken();
    }

    promise.then((isSuccess) => {
      if (isSuccess)
        queue.forEach((item) => {
          item.resolve(setToken(item.config));
        });
    });

    // 重置刷新标识, 刷新的promise, 和 待发送请求的队列
    promise.finally(() => {
      isRefreshToken = false;
      promise = null;
      queue = [];
    });
  }

  return { handleUnauthorized, handleReqWhenUnauthorized };
}

参考资料