前端实现token无感刷新

1.在common.ts定义刷新token的接口

javascript 复制代码
export function getAuthToken(data?: Object) {
	return request({
		url: '/admin/oauth2/retoken',
		method: 'post',
		data,
	});
}

2.创建token.ts文件

javascript 复制代码
import Cookies from 'js-cookie';
import { getAuthToken } from '/@/api/common'
import { ElMessage } from 'element-plus';

let timer;

export interface TokenData {
  accessToken: string;
  refreshToken: string;
  expiresIn?: number;
}

class TokenManager {
  private accessToken: string | null = null;
  private refreshToken: string | null = null;
  private refreshPromise: Promise<TokenData> | null = null;

  constructor() {
    this.loadTokens();
  }

  // 从 Cookies 加载 tokens
  private loadTokens() {
    this.accessToken = Cookies.get('access_token');
    this.refreshToken = Cookies.get('refresh_token');
  }

  // 设置 tokens
  setTokens(tokens: TokenData): void {
    this.accessToken = tokens.accessToken;
    this.refreshToken = tokens.refreshToken;
    
    Cookies.set('access_token', tokens.accessToken);
    Cookies.set('refresh_token', tokens.refreshToken);
    
    if (tokens.expiresIn) {
      const expireTime = Date.now() / 1000 + tokens.expiresIn;
      Cookies.set('tokenAgeing', expireTime.toString());
    }
  }

  // 获取 access token
  getAccessToken(): string | null {
    return Cookies.get('access_token');
  }

  // 获取 refresh token
  getRefreshToken(): string | null {
    return Cookies.get('refresh_token');
  }

  // 清除 Promise
  clearPromise(): void {
    this.refreshPromise = null;
  }

  // 检查 token 是否即将过期
  isTokenExpiring(): boolean {
    const expireTime = Cookies.get('tokenAgeing');
    if (!expireTime) return false;
    
    const currentTime = Date.now();
    const timeUntilExpire = parseInt(expireTime) * 1000 - currentTime;
    
    // 提前 5 分钟刷新
    return timeUntilExpire < 5 * 60 * 1000;
  }

  // 刷新 token
  async refreshTokens(): Promise<TokenData> {
    // 如果已经在刷新,返回同一个 promise
    if (this.refreshPromise) {
      return this.refreshPromise;
    }

    const refreshToken = this.getRefreshToken();
    if (!refreshToken) {
      ElMessage.error('refreshToken不存在!');
      timer = setTimeout(()=>{
        console.log('跳转到登录页')
      }, 2000)
    }

    this.refreshPromise = new Promise(async (resolve, reject) => {
      try {
        getAuthToken({ refreshToken: refreshToken }).then((res) => {
          console.log(res)
          if(res.code === 200 && res.data !== null){
            this.setTokens(res.data?.list[0]);
            resolve(res.data?.list[0]);
          }else{
            ElMessage.error('token刷新失败!');
            timer = setTimeout(()=>{
              console.log('跳转到登录页')
            }, 2000)
          }
        });
      } catch (error) {
        console.log(error)
        reject(error);
      } finally {
        this.clearPromise()
      }
    });

    return this.refreshPromise;
  }
}

export const tokenManager = new TokenManager();

3.在request.ts中进行接口拦截处理

javascript 复制代码
import axios from 'axios';
import { tokenManager } from '/@/utils/token';
import { ElMessage } from 'element-plus';

let timer;

// 使用 accessToken(短期)和 refreshToken(长期)

// 请求拦截器
axios.interceptors.request.use(config => {
  // 统一增加Authorization请求头
  const token = tokenManager.getAccessToken();
  config.headers.Authorization = `Bearer ${token}`;

  // 如果 token 即将过期,提前刷新
  if (tokenManager.isTokenExpiring() && !config.url?.includes('/admin/oauth2/retoken')) {
      try {
        await tokenManager.refreshTokens();
        const newToken = tokenManager.getAccessToken();
        if (newToken) {
          config.headers.Authorization = `Bearer ${newToken}`;
        }
      } catch (error) {
        // 刷新失败
        tokenManager.clearTokens();

        ElMessage.error('token刷新失败!');
        timer = setTimeout(()=>{
          console.log('跳转到登录页')
        }, 2000)
      }
    }
  return config;
});

// 创建请求唯一标识符
const createRequestId = (config): string => {
  return `${config.method}-${config.url}-${JSON.stringify(config.data)}`;
};

// 在全局状态中管理重试记录
const retryRecords = new Map<string, number>();

// 响应拦截器处理函数
const handleResponse = (response) => {
  // 请求成功
  const requestId = createRequestId(response.config);
  retryRecords.delete(requestId);//清除重试记录
  tokenManager.clearPromise() //清除promise
	return response.data;
};

// 响应拦截器
axios.interceptors.response.use(
  response => handleResponse(response ),
  async error => {
    const originalRequest = error.config;
    const requestId = createRequestId(originalRequest);
    
    // 检查重试次数
    const retryCount = retryRecords.get(requestId) || 0;
    // 如果响应状态码为401,且请求的URL不是刷新token的接口
    if (error.response?.status === 401 && !originalRequest.url?.includes('/admin/oauth2/retoken') && retryCount < 1) {
      retryRecords.set(requestId, retryCount + 1);
      
      try {
        // 刷新 token
        await tokenManager.refreshTokens();
        const newToken = tokenManager.getAccessToken();
        if (newToken) {
          // 更新 Authorization 头
          config.headers.Authorization = `Bearer ${newToken}`
          // 重试原始请求
          return axios(originalRequest);
        }
      } catch (refreshError) {
        retryRecords.delete(requestId);
        // 刷新失败
        tokenManager.clearTokens();

        ElMessage.error('token刷新失败!');
        timer = setTimeout(()=>{
          console.log('跳转到登录页')
        }, 2000)
        return Promise.reject(refreshError);
      }
    }
    
    retryRecords.delete(requestId);
    return Promise.reject(error);
  }
);
相关推荐
浮生望3 分钟前
前端路由进阶:History API原理与手写HistoryRouter
前端
陆枫Larry25 分钟前
JavaScript 中的竞态是什么,为啥会有竟态?
前端
上海安当技术3 小时前
半天接入:USBKey RESTful API + C 动态库,Web 和 C/S 两套集成路径实战
前端·后端·restful·集成·usbkey
DevUI团队3 小时前
从“即兴创作”到“规格先行”,华为云码道(CodeArts)代码智能体持续深耕企业级规范驱动开发能力
前端·人工智能·后端
weixin_431600444 小时前
NestJS 入门(3):Guard 如何挡住未登录请求?
前端·后端·学习·nest.js
avi91114 小时前
[AI教做人]AI平台做2项目;一个3D模型展示,另一个框架多人
javascript·人工智能·ai·3d模型·3d引擎·顶点和法线
kyriewen4 小时前
我用Claude Code两天干完了团队两周的排期——周报发出去那一刻我就后悔了
前端·javascript·ai编程
IT_陈寒5 小时前
JavaScript类型转换把我坑惨了,这破玩意真该早点搞明白
前端·人工智能·后端
用户938515635075 小时前
Type vs Interface:读完这篇就没有面试官能难倒你了
前端·面试·typescript
用户938515635075 小时前
手写一个 LLM Harness 框架:用工程化手段把大模型幻觉踩在脚下
javascript·人工智能·后端