前端 Token 无感刷新全解析:Vue3 与 React 实现方案

在前后端分离架构中,Token 是主流的身份认证方式。但 Token 存在有效期限制,若在用户操作过程中 Token 过期,会导致请求失败,影响用户体验。「无感刷新」技术应运而生------它能在 Token 过期前或过期瞬间,自动刷新 Token 并继续完成原请求,全程对用户透明。

本文将先梳理 Token 无感刷新的核心原理,再分别基于 Vue3(Composition API + Pinia)和 React(Hooks + Axios)给出完整实现方案,同时解析常见问题与优化思路,帮助开发者快速落地。

一、核心原理:为什么需要无感刷新?怎么实现?

1. 基础概念:Access Token 与 Refresh Token

无感刷新依赖「双 Token 机制」,后端需返回两种 Token:

  • Access Token(访问 Token) :有效期短(如 2 小时),用于接口请求的身份认证,放在请求头(如 Authorization: Bearer {token});
  • Refresh Token(刷新 Token) :有效期长(如 7 天),仅用于 Access Token 过期时请求新的 Access Token,安全性要求更高(建议存储在 HttpOnly Cookie 中,避免 XSS 攻击)。

2. 无感刷新核心流程

  1. 前端发起接口请求,携带 Access Token;
  2. 拦截响应:若返回 401 状态码(Access Token 过期),则触发刷新逻辑;
  3. 用 Refresh Token 调用后端「刷新 Token 接口」,获取新的 Access Token;
  4. 更新本地存储的 Access Token;
  5. 重新发起之前失败的请求(携带新 Token);
  6. 若 Refresh Token 也过期(刷新接口返回 401),则跳转至登录页,要求用户重新登录。

关键优化点:避免重复刷新------当多个请求同时因 Token 过期失败时,需保证只发起一次 Refresh Token 请求,其他请求排队等待新 Token 生成后再重试。

二、前置准备:Axios 拦截器封装(通用基础)

无论是 Vue 还是 React,都可基于 Axios 的「请求拦截器」和「响应拦截器」实现 Token 统一处理。先封装一个基础 Axios 实例:

js 复制代码
// utils/request.js
import axios from 'axios';

// 创建 Axios 实例
const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL, // 环境变量中的接口基础地址
  timeout: 5000 // 请求超时时间
});

// 1. 请求拦截器:添加 Access Token
service.interceptors.request.use(
  (config) => {
    const accessToken = localStorage.getItem('accessToken'); // 简化存储,实际建议 Vue 用 Pinia/React 用状态管理
    if (accessToken) {
      config.headers.Authorization = `Bearer ${accessToken}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// 2. 响应拦截器:处理 Token 过期逻辑(核心,后续框架差异化实现)
// 此处先留空,后续在 Vue/React 中补充具体逻辑
service.interceptors.response.use(
  (response) => response.data, // 直接返回响应体
  (error) => handleResponseError(error, service) // 错误处理,传入 service 用于重试请求
);

export default service;

三、Vue3 实现方案(Composition API + Pinia)

Vue3 中推荐用 Pinia 管理全局状态(存储 Token),结合 Composition API 封装刷新逻辑,保证代码复用性。

1. 步骤 1:Pinia 状态管理(存储 Token)

创建 Pinia Store 管理 Access Token 和 Refresh Token,提供刷新 Token 的方法:

js 复制代码
// stores/authStore.js
import { defineStore } from 'pinia';
import axios from 'axios';

export const useAuthStore = defineStore('auth', {
  state: () => ({
    accessToken: localStorage.getItem('accessToken') || '',
    refreshToken: localStorage.getItem('refreshToken') || '' // 实际建议存 HttpOnly Cookie
  }),
  actions: {
    // 更新 Token
    updateTokens(newAccessToken, newRefreshToken) {
      this.accessToken = newAccessToken;
      this.refreshToken = newRefreshToken;
      localStorage.setItem('accessToken', newAccessToken);
      localStorage.setItem('refreshToken', newRefreshToken); // 仅演示,生产环境用 HttpOnly Cookie
    },
    // 刷新 Token 核心方法
    async refreshAccessToken() {
      try {
        const res = await axios.post('/api/refresh-token', {
          refreshToken: this.refreshToken
        });
        const { accessToken, refreshToken } = res.data;
        this.updateTokens(accessToken, refreshToken);
        return accessToken; // 返回新 Token,用于重试请求
      } catch (error) {
        // 刷新 Token 失败(如 Refresh Token 过期),清除状态并跳转登录
        this.clearTokens();
        window.location.href = '/login';
        return Promise.reject(error);
      }
    },
    // 清除 Token
    clearTokens() {
      this.accessToken = '';
      this.refreshToken = '';
      localStorage.removeItem('accessToken');
      localStorage.removeItem('refreshToken');
    }
  }
});

2. 步骤 2:实现响应拦截器的错误处理

完善之前的响应拦截器,添加 Token 过期处理逻辑,核心是「避免重复刷新」:

js 复制代码
// utils/request.js(Vue3 版本补充)
import { useAuthStore } from '@/stores/authStore';

// 用于存储刷新 Token 的请求(避免重复刷新)
let refreshPromise = null;

// 响应错误处理函数
async function handleResponseError(error, service) {
  const authStore = useAuthStore();
  const originalRequest = error.config; // 原始请求配置

  // 1. 不是 401 错误,直接 reject
  if (error.response?.status !== 401) {
    return Promise.reject(error);
  }

  // 2. 是 401 错误,但已经重试过一次,避免死循环
  if (originalRequest._retry) {
    return Promise.reject(error);
  }

  try {
    // 3. 标记当前请求已重试,避免重复
    originalRequest._retry = true;

    // 4. 若没有正在进行的刷新请求,发起刷新;否则等待已有请求完成
    if (!refreshPromise) {
      refreshPromise = authStore.refreshAccessToken();
    }

    // 5. 等待刷新完成,获取新 Token
    const newAccessToken = await refreshPromise;

    // 6. 刷新完成后,重置 refreshPromise
    refreshPromise = null;

    // 7. 更新原始请求的 Authorization 头,重新发起请求
    originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
    return service(originalRequest);
  } catch (refreshError) {
    // 刷新失败,重置 refreshPromise
    refreshPromise = null;
    return Promise.reject(refreshError);
  }
}

// 响应拦截器(补充完整)
service.interceptors.response.use(
  (response) => response.data,
  (error) => handleResponseError(error, service)
);

3. 步骤 3:组件中使用

封装好后,组件中直接使用 request 发起请求即可,无需关注 Token 刷新逻辑:

xml 复制代码
// components/Example.vue
<script setup>
import request from '@/utils/request';
import { ref, onMounted } from 'vue';

const data = ref(null);

onMounted(async () => {
  try {
    // 发起请求,Token 过期时会自动无感刷新
    const res = await request.get('/api/user-info');
    data.value = res.data;
  } catch (error) {
    console.error('请求失败:', error);
  }
});
</script>

<template>
  <div>{{ data ? data.name : '加载中...' }}</div>
</template>

四、React 实现方案(Hooks + Context)

React 中推荐用「Context + Hooks」管理全局 Token 状态,结合 Axios 拦截器实现无感刷新,逻辑与 Vue3 类似,但状态管理方式不同。

1. 步骤 1:创建 Auth Context(管理 Token 状态)

用 Context 提供 Token 相关的状态和方法,供全局组件使用:

js 复制代码
// context/AuthContext.js
import { createContext, useContext, useState, useEffect } from 'react';
import axios from 'axios';

// 创建 Context
const AuthContext = createContext();

//  Provider 组件:提供 Token 状态和方法
export function AuthProvider({ children }) {
  const [accessToken, setAccessToken] = useState(localStorage.getItem('accessToken') || '');
  const [refreshToken, setRefreshToken] = useState(localStorage.getItem('refreshToken') || '');

  // 更新 Token
  const updateTokens = (newAccessToken, newRefreshToken) => {
    setAccessToken(newAccessToken);
    setRefreshToken(newRefreshToken);
    localStorage.setItem('accessToken', newAccessToken);
    localStorage.setItem('refreshToken', newRefreshToken); // 演示用,生产环境用 HttpOnly Cookie
  };

  // 刷新 Token
  const refreshAccessToken = async () => {
    try {
      const res = await axios.post('/api/refresh-token', { refreshToken });
      const { accessToken: newAccessToken, refreshToken: newRefreshToken } = res.data;
      updateTokens(newAccessToken, newRefreshToken);
      return newAccessToken;
    } catch (error) {
      // 刷新失败,清除状态并跳转登录
      clearTokens();
      window.location.href = '/login';
      return Promise.reject(error);
    }
  };

  // 清除 Token
  const clearTokens = () => {
    setAccessToken('');
    setRefreshToken('');
    localStorage.removeItem('accessToken');
    localStorage.removeItem('refreshToken');
  };

  // 提供给子组件的内容
  const value = {
    accessToken,
    refreshToken,
    updateTokens,
    refreshAccessToken,
    clearTokens
  };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}

// 自定义 Hook:方便组件获取 Auth 状态
export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
}

2. 步骤 2:在入口文件中包裹 AuthProvider

确保全局组件都能访问到 Auth Context:

js 复制代码
// index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { AuthProvider } from './context/AuthContext';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <AuthProvider>
    <App />
  </AuthProvider>
);

3. 步骤 3:完善 Axios 响应拦截器

逻辑与 Vue3 一致,核心是避免重复刷新,通过 useAuth Hook 获取刷新 Token 方法:

js 复制代码
// utils/request.js(React 版本补充)
import { useAuth } from '../context/AuthContext';

// 注意:React 中不能在 Axios 拦截器中直接使用 useAuth(Hook 只能在组件/自定义 Hook 中使用)
// 解决方案:用一个函数封装,在组件初始化时调用,注入 auth 实例
export function initRequestInterceptors() {
  const { refreshAccessToken } = useAuth();
  let refreshPromise = null;

  // 响应错误处理函数
  async function handleResponseError(error, service) {
    const originalRequest = error.config;

    // 1. 非 401 错误,直接 reject
    if (error.response?.status !== 401) {
      return Promise.reject(error);
    }

    // 2. 已重试过,避免死循环
    if (originalRequest._retry) {
      return Promise.reject(error);
    }

    try {
      originalRequest._retry = true;

      // 3. 避免重复刷新
      if (!refreshPromise) {
        refreshPromise = refreshAccessToken();
      }

      // 4. 等待新 Token
      const newAccessToken = await refreshPromise;
      refreshPromise = null;

      // 5. 重试原始请求
      originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
      return service(originalRequest);
    } catch (refreshError) {
      refreshPromise = null;
      return Promise.reject(refreshError);
    }
  }

  // 重新设置响应拦截器(注入 auth 实例后)
  service.interceptors.response.use(
    (response) => response.data,
    (error) => handleResponseError(error, service)
  );
}

export default service;

4. 步骤 4:在组件中初始化拦截器并使用

在根组件(如 App.js)中初始化拦截器,确保 useAuth 能正常使用:

js 复制代码
// App.js
import { useEffect } from 'react';
import { initRequestInterceptors } from './utils/request';
import request from './utils/request';
import { useState } from 'react';

function App() {
  const [userInfo, setUserInfo] = useState(null);

  // 初始化 Axios 拦截器(注入 Auth 上下文)
  useEffect(() => {
    initRequestInterceptors();
  }, []);

  // 发起请求(Token 过期自动刷新)
  const fetchUserInfo = async () => {
    try {
      const res = await request.get('/api/user-info');
      setUserInfo(res.data);
    } catch (error) {
      console.error('请求失败:', error);
    }
  };

  useEffect(() => {
    fetchUserInfo();
  }, []);

  return (
    <div className="App">
      {userInfo ? <h1>欢迎,{userInfo.name}</h1> : <p>加载中...</p>}
    </div>
  );
}

export default App;

五、关键优化与安全注意事项

1. 避免重复刷新的核心逻辑

用「refreshPromise」变量存储正在进行的刷新 Token 请求,当多个请求同时失败时,都等待同一个 refreshPromise 完成,避免发起多个刷新请求,这是无感刷新的核心优化点。

2. 安全优化:Refresh Token 的存储方式

  • 不建议将 Refresh Token 存储在 localStorage/sessionStorage 中,容易遭受 XSS 攻击;

  • 推荐存储在「HttpOnly Cookie」中,由浏览器自动携带,无法通过 JavaScript 访问,有效防御 XSS 攻击;

  • 若后端支持,可给 Refresh Token 增加「设备绑定」「IP 限制」等额外安全措施。

3. 主动刷新:提前预防 Token 过期

被动刷新(等待 401 后再刷新)可能存在延迟,可增加「主动刷新」逻辑:

  • 记录 Access Token 的生成时间和过期时间;
  • 在请求拦截器中判断 Token 剩余有效期(如小于 5 分钟),主动发起刷新请求;
  • 避免在用户无操作时刷新,可结合「用户活动监听」(如 click、keydown 事件)触发主动刷新。

4. 异常处理:刷新失败的兜底方案

当 Refresh Token 过期或无效时,必须跳转至登录页,并清除本地残留的 Token 状态,避免死循环请求。同时,可给用户提示「登录已过期,请重新登录」,提升体验。

六、Vue3 与 React 实现方案对比

对比维度 Vue3 实现 React 实现
状态管理 Pinia(官方推荐,API 简洁,支持 TypeScript) Context + Hooks(原生支持,无需额外依赖)
拦截器初始化 可直接在 Pinia 中获取状态,无需额外注入 需在组件中初始化拦截器,注入 Auth Context
核心逻辑 基于 Composition API,逻辑封装更灵活 基于自定义 Hooks,符合函数式编程思想
学习成本 Pinia 学习成本低,适合 Vue 生态开发者 Context + Hooks 需理解 React 状态传递机制

本质差异:状态管理方式不同,但无感刷新的核心逻辑(双 Token、拦截器、避免重复刷新)完全一致,开发者可根据自身技术栈选择对应方案。

七、总结

前端 Token 无感刷新的核心是「双 Token 机制 + Axios 拦截器」,关键在于解决「重复刷新」和「安全存储」问题。Vue3 和 React 的实现方案虽在状态管理上有差异,但核心逻辑相通:

  1. 用请求拦截器统一添加 Access Token;
  2. 用响应拦截器捕获 401 错误,触发刷新逻辑;
  3. 通过一个全局变量控制刷新请求的唯一性,避免重复请求;
  4. 刷新成功后重试原始请求,失败则跳转登录。

实际项目中,需结合后端接口设计(如刷新 Token 的接口地址、参数格式)和安全需求(如 Refresh Token 存储方式)调整实现细节。合理的无感刷新方案能大幅提升用户体验,避免因 Token 过期导致的操作中断。

相关推荐
wayne2142 小时前
React Native 2025 年度回顾:架构、性能与生态的全面升级
react native·react.js·架构
雲墨款哥2 小时前
React小demo,评论列表
前端·react.js
UIUV2 小时前
React表单处理:受控组件与非受控组件全面解析
前端·javascript·react.js
不想秃头的程序员2 小时前
Vue 与 React 数据体系深度对比
前端·vue.js
成为大佬先秃头3 小时前
渐进式JavaScript框架:Vue — API
开发语言·javascript·vue.js
Komorebi゛4 小时前
【Vue3 + Element Plus】Form表单按下Enter键导致页面刷新问题
前端·javascript·vue.js
dly_blog4 小时前
Vue 组件通信方式大全(第7节)
前端·javascript·vue.js
郭小铭4 小时前
将 Markdown 文件导入为 React 组件 - 写作文档,即时获取交互式演示
前端·react.js·markdown
橙某人4 小时前
LogicFlow 交互新体验:告别直连,丝滑贝塞尔轨迹实战!🍫
前端·javascript·vue.js