You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

刷新令牌过期后如何重定向至登录页?解决React+Axios无限循环问题

问题分析

  1. Axios无法使用React Router跳转:Axios模块是纯JS文件,无法直接访问React Router的useNavigate钩子,直接用window.location.href会导致页面刷新,不符合SPA路由跳转逻辑。
  2. 无限循环崩溃:refresh token过期时,refreshAccessToken请求失败后跳转登录页,但未清除本地token和Redux登录状态,后续未完成的API请求会持续触发401拦截,重复调用刷新逻辑,最终形成循环导致浏览器崩溃。

解决方案

1. 用Redux同步状态+组件监听实现路由跳转

在Axios拦截器中触发Redux的logout action清除状态,再在App组件中监听登录状态变化,自动完成SPA路由跳转。

修改axios.js文件

import axios from "axios";
import { store } from "./redux/store"; // 导入Redux store实例
import { logout } from "./redux/actions/authActions";

const BASE_URL = "http://localhost:8083/api/v1";

const axiosInstance = axios.create({
  baseURL: BASE_URL,
});

const refreshAccessToken = async () => {
  try {
    const refresh_token = localStorage.getItem("refresh_token");
    // 用原生axios发起刷新请求,避免触发自定义拦截器导致循环
    const response = await axios.get(`${BASE_URL}/auth/refresh`, {
      headers: {
        Authorization: `Bearer ${refresh_token}`,
      },
    });
    const access_token = response.data.access_token;
    localStorage.setItem("access_token", access_token);
    return access_token;
  } catch (error) {
    // 清除本地存储的所有token
    localStorage.removeItem("access_token");
    localStorage.removeItem("refresh_token");
    // 触发logout action更新全局登录状态
    store.dispatch(logout());
    return Promise.reject(error);
  }
};

axiosInstance.interceptors.request.use(
  async (config) => {
    if (!config.headers.Authorization) {
      const access_token = localStorage.getItem("access_token");
      if (access_token) {
        config.headers = config.headers || {};
        config.headers.Authorization = `Bearer ${access_token}`;
      }
    }
    return config;
  },
  (error) => Promise.reject(error)
);

axiosInstance.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error?.config;
    // 仅处理非刷新请求的401,且确保同一请求只重试一次
    if (
      error.response?.status === 401 &&
      !originalRequest?.sent &&
      originalRequest?.url !== "/auth/refresh"
    ) {
      originalRequest.sent = true;
      try {
        const new_access_token = await refreshAccessToken();
        originalRequest.headers.Authorization = `Bearer ${new_access_token}`;
        return axiosInstance(originalRequest);
      } catch (refreshError) {
        // 刷新失败直接拒绝原请求,不再重试
        return Promise.reject(refreshError);
      }
    }
    // 处理刷新请求本身的401
    if (error.response?.status === 401 && originalRequest?.url === "/auth/refresh") {
      localStorage.removeItem("access_token");
      localStorage.removeItem("refresh_token");
      store.dispatch(logout());
    }
    return Promise.reject(error);
  }
);

export default axiosInstance;

修改App.jsx文件

import Login from "./components/Login";
import Register from "./components/Register";
import Topics from "./components/Topics";
import Root from "./components/Root";
import Dashboard from "./components/dashboard";
import { Routes, Route, useNavigate, Navigate } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { login, logout } from "./redux/actions/authActions";
import React, { useEffect } from "react";
import EmployeeList from "./components/EmployeeList";

function App() {
  const isLoggedIn = useSelector((state) => state.auth.isLoggedIn);
  const dispatch = useDispatch();
  const navigate = useNavigate();

  // 监听登录状态,未登录时自动跳转到登录页
  useEffect(() => {
    if (!isLoggedIn) {
      navigate("/", { replace: true });
    }
  }, [isLoggedIn, navigate]);

  const handleLogin = () => {
    dispatch(login());
  };

  const handleLogout = () => {
    dispatch(logout());
  };

  return (
    <Routes>
      {/* 未登录显示登录页,已登录则跳转到主页 */}
      <Route path="/" element={!isLoggedIn ? <Login onLogin={handleLogin} /> : <Navigate to="/topics" replace />} />
      {/* 已登录时的路由组 */}
      {isLoggedIn && (
        <Route path="/" element={<Root onLogout={handleLogout} />}>
          <Route index element={<Topics />} />
          <Route path="employeesList" element={<EmployeeList />} />
          <Route path="register" element={<Register />} />
          <Route path="dashboard" element={<Dashboard />} />
        </Route>
      )}
      {/* 兜底路由重定向 */}
      <Route path="*" element={isLoggedIn ? <Navigate to="/" replace /> : <Navigate to="/" replace />} />
    </Routes>
  );
}

export default App;

2. 关键优化点说明

  • 避免拦截器循环:刷新token的请求使用原生axios,防止触发自定义拦截器;同时排除/auth/refresh路径的401处理逻辑。
  • 状态全局同步:refresh失败时清除本地token并触发Redux的logout action,确保所有组件同步感知登录状态变化。
  • SPA友好跳转:通过useEffect监听登录状态变化,结合useNavigate实现无刷新路由跳转,替代window.location.href的页面刷新方式。
  • 防止重复重试:给原请求添加sent标记,确保同一请求只触发一次刷新逻辑,避免重复请求导致的循环。

3. 额外建议

  • 登录组件成功登录后,要正确存储access_token和refresh_token到本地存储,并触发login action更新Redux状态。
  • 可添加请求取消逻辑,当用户注销或token过期时,取消所有pending请求,进一步减少不必要的拦截触发。

内容的提问来源于stack exchange,提问作者raio

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 23:35:14