React+Node.js中JWT登录刷新后状态丢失问题求助
问题原因分析
页面刷新时,React的useState会重置为初始值(空字符串),此时RoleContext中的accessToken为空,ProtectedRoute检测到无有效token就会强制跳转到登录页。虽然refreshToken存储在httpOnly Cookie中,但你的代码没有在页面加载时自动触发用refreshToken换取新accessToken的逻辑,导致Context中没有token,路由拦截生效。
另外,你的Axios拦截器存在两个明显错误:
- 请求拦截器中直接访问
RoleContext.accessToken是错误的,RoleContext是Context对象,不是它的value,无法直接获取token。 - 响应拦截器中直接调用
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
相关产品推荐
相关产品推荐

