前端实现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);
  }
);
相关推荐
酒鼎25 分钟前
学习笔记(12-02)事件循环 - 实战案例 —⭐
前端·javascript
Bigger30 分钟前
第一章:我是如何剖析 Claude Code 整体架构与启动流程的
前端·aigc·claude
小恰学逆向36 分钟前
【爬虫JS逆向之旅】某球网参数“md5__1038”逆向
javascript·爬虫
竹林81836 分钟前
从“连接失败”到丝滑登录:我用 ethers.js v6 搞定 MetaMask 钱包连接的全过程
前端·javascript
oi..40 分钟前
《Web 安全入门|XSS 漏洞原理、CSP 策略与 HttpOnly 防护实践》
前端·网络·测试工具·安全·web安全·xss
UXbot1 小时前
2026年AI全链路产品开发工具对比:5款从创意到上线一站式平台深度解析
前端·ui·kotlin·软件构建·swift·原型模式
一拳不是超人1 小时前
前端工程师也要懂的服务器部署知识:从 Nginx 到 CI/CD
服务器·前端
AlkaidSTART1 小时前
TanStack Query 技术指南:异步状态管理核心实践
前端·react.js
三原1 小时前
超级好用的三原后台管理v1.0.0发布🎉(Vue3 + Ant Design Vue + Java Spring Boot )附源码
java·vue.js·开源
前端那点事1 小时前
前端必看!JS高频实用案例(单行代码+实战场景+十大排序)
javascript