JWT令牌刷新后如何获取认证数据?React项目问题求助
React JWT认证:解决页面加载令牌刷新竞态问题及优化方案
一、解决页面加载时请求提前发起的问题
1. 在AuthContext中维护认证加载状态
在你的认证上下文里加一个isLoading状态,初始设为true。页面加载时先判断本地accessToken是否过期:
- 若已过期,优先调用
updateTokens刷新令牌,完成后把isLoading设为false - 若未过期,直接标记加载完成,同时启动定时刷新任务
示例代码:
// AuthContext.js import { createContext, useContext, useState, useEffect } from 'react'; const AuthContext = createContext(); export const AuthProvider = ({ children }) => { const [accessToken, setAccessToken] = useState(localStorage.getItem('accessToken')); const [isLoading, setIsLoading] = useState(true); const updateTokens = async () => { try { const refreshToken = localStorage.getItem('refreshToken'); const res = await fetch('/api/refresh-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refreshToken }) }); const data = await res.json(); setAccessToken(data.accessToken); localStorage.setItem('accessToken', data.accessToken); } catch (err) { // 刷新失败则清除令牌,跳转登录页 localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); window.location.href = '/login'; } finally { setIsLoading(false); } }; useEffect(() => { const checkTokenValidity = () => { const token = localStorage.getItem('accessToken'); if (!token) { setIsLoading(false); return; } // 解析token payload判断是否过期 const payload = JSON.parse(atob(token.split('.')[1])); const currentTime = Date.now() / 1000; if (payload.exp < currentTime) { updateTokens(); } else { setIsLoading(false); // 提前1分钟刷新,避免刚好在请求时过期 const refreshTimer = setInterval(updateTokens, (payload.exp - currentTime - 60) * 1000); return () => clearInterval(refreshTimer); } }; checkTokenValidity(); }, []); return ( <AuthContext.Provider value={{ accessToken, isLoading }}> {children} </AuthContext.Provider> ); }; export const useAuth = () => useContext(AuthContext);
2. 页面组件等待加载完成再发起请求
在需要调用getRooms的组件里,先判断isLoading状态,等认证完成后再执行数据请求:
// RoomsPage.js import { useEffect, useState } from 'react'; import { useAuth } from './AuthContext'; const RoomsPage = () => { const { accessToken, isLoading } = useAuth(); const [rooms, setRooms] = useState([]); const fetchRooms = async () => { if (!accessToken) return; const res = await fetch('/api/rooms', { headers: { Authorization: `Bearer ${accessToken}` } }); if (res.ok) { const data = await res.json(); setRooms(data); } }; useEffect(() => { if (!isLoading) fetchRooms(); }, [accessToken, isLoading]); if (isLoading) { return <div>加载中...</div>; } return ( <div className="rooms-list"> {rooms.map(room => <div key={room.id}>{room.name}</div>)} </div> ); }; export default RoomsPage;
3. 封装请求拦截器统一处理(推荐)
如果用Axios,可以给请求加拦截器,统一处理令牌检查和刷新,避免每个组件重复写逻辑:
// api.js import axios from 'axios'; import { useAuth } from './AuthContext'; const api = axios.create({ baseURL: '/api' }); // 维护一个刷新Promise,防止并发请求重复刷新 let refreshPromise = null; api.interceptors.request.use(async (config) => { const { accessToken, isLoading, updateTokens } = useAuth(); // 等待认证加载完成 if (isLoading) { await new Promise(resolve => { const interval = setInterval(() => { if (!isLoading) { clearInterval(interval); resolve(); } }, 50); }); } const token = localStorage.getItem('accessToken'); if (!token) return config; const payload = JSON.parse(atob(token.split('.')[1])); const currentTime = Date.now() / 1000; // 令牌即将过期且无正在进行的刷新请求,触发刷新 if (payload.exp < currentTime + 60 && !refreshPromise) { refreshPromise = updateTokens().finally(() => { refreshPromise = null; }); await refreshPromise; // 更新请求头的新令牌 config.headers.Authorization = `Bearer ${localStorage.getItem('accessToken')}`; } return config; }); export default api;
二、更优的认证与令牌刷新方案
1. 替换定时刷新为“请求时检查+自动重试”
不要固定每4分钟刷新,而是在每次请求发起前检查令牌状态:
- 令牌有效:直接发起请求
- 令牌过期:先刷新令牌,再用新令牌重试原请求
- 用拦截器统一处理,减少冗余代码,避免不必要的刷新请求
2. 优化令牌存储与安全策略
- accessToken:存在内存(Context状态)而非localStorage,降低XSS攻击风险
- refreshToken:存在HttpOnly Cookie中,由后端通过Cookie接收,避免前端直接操作
- 后端给refreshToken设置有效期(比如7天),同时维护refreshToken黑名单,支持主动吊销
3. 结合React Suspense处理异步认证(React 18+)
用Suspense让组件等待认证完成后再渲染,替代手动判断isLoading:
// AuthContext.js 改造 export const fetchAuthData = async () => { const token = localStorage.getItem('accessToken'); if (!token) return { accessToken: null }; const payload = JSON.parse(atob(token.split('.')[1])); const currentTime = Date.now() / 1000; if (payload.exp < currentTime) { const res = await fetch('/api/refresh-token', { method: 'POST', credentials: 'include' // 携带HttpOnly Cookie中的refreshToken }); const data = await res.json(); localStorage.setItem('accessToken', data.accessToken); return { accessToken: data.accessToken }; } return { accessToken: token }; }; export const AuthProvider = ({ children }) => { const [authData, setAuthData] = useState(null); useEffect(() => { fetchAuthData().then(setAuthData); }, []); if (!authData) { throw fetchAuthData(); // 抛给Suspense处理加载状态 } return ( <AuthContext.Provider value={authData}> {children} </AuthContext.Provider> ); }; // 页面使用Suspense import { Suspense } from 'react'; import RoomsPage from './RoomsPage'; const App = () => { return ( <AuthProvider> <Suspense fallback={<div>加载中...</div>}> <RoomsPage /> </Suspense> </AuthProvider> ); };
内容的提问来源于stack exchange,提问作者Thomas Michaelides
相关产品推荐
相关产品推荐

