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

React+Node.js中JWT登录刷新后状态丢失问题求助

问题原因分析

页面刷新时,React的useState会重置为初始值(空字符串),此时RoleContext中的accessToken为空,ProtectedRoute检测到无有效token就会强制跳转到登录页。虽然refreshToken存储在httpOnly Cookie中,但你的代码没有在页面加载时自动触发用refreshToken换取新accessToken的逻辑,导致Context中没有token,路由拦截生效。

另外,你的Axios拦截器存在两个明显错误:

  1. 请求拦截器中直接访问RoleContext.accessToken是错误的,RoleContext是Context对象,不是它的value,无法直接获取token。
  2. 响应拦截器中直接调用useContext和useNavigate,这些React Hooks只能在组件或自定义Hooks中使用,不能在Axios拦截器这类非组件代码中调用。
修复方案

1. 在RoleProvider中添加页面加载时的Token刷新逻辑

修改你的RoleProvider,在组件初始化时自动调用refreshToken接口,获取新的accessToken并更新Context状态:

import { createContext, useState, useEffect } from 'react';
import axiosInstance from './axiosInstance';

export const RoleContext = createContext({
  accessToken: '',
  setAccessToken: () => {}
});

export const RoleProvider = ({ children }) => {
  const [accessToken, setAccessToken] = useState('');

  useEffect(() => {
    // 页面加载时尝试刷新token
    const refreshToken = async () => {
      try {
        const res = await axiosInstance.post('/auth/token');
        setAccessToken(res.data.accessToken);
      } catch (err) {
        // 刷新失败说明refreshToken无效,无需处理,留在登录页即可
        console.error('Token refresh failed on page load:', err);
      }
    };

    // 只有当accessToken为空时才触发刷新
    if (!accessToken) {
      refreshToken();
    }
  }, [accessToken]);

  return (
    <RoleContext.Provider value={{ accessToken, setAccessToken }}>
      {children}
    </RoleContext.Provider>
  );
};

2. 修复Axios拦截器的Hook使用错误

将拦截器的设置逻辑改为在组件内调用,避免在非组件代码中使用React Hooks:

// axiosInstance.js
import axios from 'axios';

const axiosInstance = axios.create({
  baseURL: process.env.REACT_APP_API_BASE_URL || "http://localhost:3000",
  withCredentials: true,
});

// 暴露一个设置拦截器的方法,接收组件中的accessToken、setAccessToken和navigate
export const setupAxiosInterceptors = (accessToken, setAccessToken, navigate) => {
  // 清除已有拦截器,防止重复添加
  axiosInstance.interceptors.request.eject(axiosInstance.interceptors.request.handlers[0]?.id);
  axiosInstance.interceptors.response.eject(axiosInstance.interceptors.response.handlers[0]?.id);

  // 请求拦截器:添加Authorization头
  axiosInstance.interceptors.request.use(
    (config) => {
      if (accessToken) {
        config.headers["Authorization"] = `Bearer ${accessToken}`;
      }
      return config;
    },
    (error) => Promise.reject(error)
  );

  // 响应拦截器:处理401错误,刷新token
  axiosInstance.interceptors.response.use(
    (response) => response,
    async (error) => {
      const { config, response } = error;
      const originalRequest = config;

      if (response?.status === 401 && !originalRequest._retry) {
        originalRequest._retry = true;
        try {
          const res = await axiosInstance.post("/auth/token");
          const newToken = res.data.accessToken;
          setAccessToken(newToken);
          originalRequest.headers["Authorization"] = `Bearer ${newToken}`;
          return axiosInstance(originalRequest);
        } catch (err) {
          navigate("/auth/login");
          return Promise.reject(err);
        }
      }
      return Promise.reject(error);
    }
  );
};

export default axiosInstance;

然后在AppRoutes中调用这个方法:

const AppRoutes = () => {
  const { accessToken, setAccessToken } = useContext(RoleContext);
  const navigate = useNavigate();

  // 当accessToken变化时重新设置拦截器
  useEffect(() => {
    setupAxiosInterceptors(accessToken, setAccessToken, navigate);
  }, [accessToken, setAccessToken, navigate]);

  let role = "";
  if (accessToken !== "") {
    const decodedPayLoad = jwtDecode(accessToken);
    role = decodedPayLoad.role;
  }

  const isAuthenticated = !!accessToken;

  return (
    <Routes>
      {/* 你的路由配置 */}
    </Routes>
  );
};

3. 给ProtectedRoute添加加载状态

避免在token刷新完成前就执行跳转逻辑:

import { useState, useEffect } from 'react';
import { Navigate } from 'react-router-dom';
import { useContext } from 'react';
import { RoleContext } from './RoleContext';
import jwtDecode from 'jwt-decode';

const ProtectedRoute = ({ element, allowedRoles }) => {
  const { accessToken } = useContext(RoleContext);
  const [isLoading, setIsLoading] = useState(true);
  const [role, setRole] = useState('');

  useEffect(() => {
    if (accessToken) {
      try {
        const decodedPayload = jwtDecode(accessToken);
        setRole(decodedPayload.role);
      } catch (err) {
        console.error('Token decode failed:', err);
      }
    }
    setIsLoading(false);
  }, [accessToken]);

  if (isLoading) {
    return <div>加载中...</div>; // 可替换为自定义加载组件
  }

  if (!accessToken) {
    return <Navigate to="/auth/login" replace />;
  }

  if (!allowedRoles.includes(role)) {
    return <Navigate to="/" replace />;
  }

  return element;
};

export default ProtectedRoute;
验证逻辑

页面刷新时,RoleProvider会自动调用/auth/token接口,用httpOnly Cookie中的refreshToken换取新的accessToken,更新Context中的状态。此时ProtectedRoute会等待token加载完成,确认有有效token后再渲染对应的页面,不会直接跳转到登录页。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 14:33:10