React应用前端Refresh Token处理的正确实现与最佳实践指引
全栈应用中Refresh Token的React前端实现问题与最佳实践
背景
我正在开发一款使用Access Token和Refresh Token的全栈应用,已成功实现Access Token的后端逻辑,但在Refresh Token的前端处理上遇到困难。
已完成的后端接口
用户登录返回令牌接口
const accessToken = jwt.sign( { userId: foundUsername.rows[0].id_persona, username: foundUsername.rows[0].username, roles: foundRoles.rows, }, process.env.JWT_SECRET_KEY as string, { expiresIn: "5m" } ); const refreshToken = jwt.sign( { userId: foundUsername.rows[0].id_persona, username: foundUsername.rows[0].username, }, process.env.JWT_SECRET_KEY as string, { expiresIn: "1d", } ); res.cookie("refresh_cookie", refreshToken, { httpOnly: true, secure: false, sameSite: "none", maxAge: 24 * 60 * 60 * 1000, }); return res.json({ accessToken });
刷新Access Token接口
export const refreshAccess = async (req: Request, res: Response) => { // Check if the token exists const token = req.cookies["refresh_token"]; if (!token) { return res.status(401).json({ message: "Unauthorized" }); } try { const decoded = jwt.verify( token, process.env.JWT_SECRET_KEY as string ) as JwtPayload; const result = await pool.query( "SELECT id_roles FROM assignated_roles WHERE id_persona = $1", [decoded.userId] ); const foundRoles = result.rows.map((row) => row.id_roles); const accessToken = jwt.sign( { userId: decoded.userId, username: decoded.username, roles: foundRoles, }, process.env.JWT_SECRET_KEY as string, { expiresIn: "5m" } ); return res.json({ accessToken }); } catch (err) { return res.status(403).json({ message: "Invalid token" }); } };
注:后端刷新接口读取的cookie名称为
refresh_token,但登录接口设置的cookie名称是refresh_cookie,需保持一致,否则刷新接口无法正确获取Refresh Token。
前端困惑点
- 是否需要用定时器自动刷新Access Token?
- 如何用Context管理令牌,实现受保护路由?
- 担心刷新接口会频繁请求数据库,有没有优化方案?
React前端实现最佳实践与具体指引
1. 令牌管理:Context + LocalStorage 统一维护
创建AuthContext封装登录状态、Access Token的读写逻辑,实现全局状态共享:
import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; interface AuthContextType { accessToken: string | null; login: (token: string) => void; logout: () => void; } const AuthContext = createContext<AuthContextType | undefined>(undefined); export const AuthProvider = ({ children }: { children: ReactNode }) => { const [accessToken, setAccessToken] = useState<string | null>( localStorage.getItem('accessToken') ); const login = (token: string) => { setAccessToken(token); localStorage.setItem('accessToken', token); }; const logout = () => { setAccessToken(null); localStorage.removeItem('accessToken'); // 可选:调用后端接口清除refresh cookie fetch('/api/logout', { method: 'POST' }); }; return ( <AuthContext.Provider value={{ accessToken, login, logout }}> {children} </AuthContext.Provider> ); }; export const useAuth = () => { const context = useContext(AuthContext); if (!context) { throw new Error('useAuth必须在AuthProvider内部使用'); } return context; };
2. 刷新Access Token的两种方案
方案一:请求拦截+过期自动刷新(推荐)
无需定时器,在请求拦截器中检查Access Token有效期,即将过期时自动刷新,同时处理并发请求避免重复刷新:
import axios from 'axios'; const api = axios.create({ baseURL: '/api' }); // 防止并发请求重复刷新token let isRefreshing = false; let failedQueue: { resolve: (token: string) => void; reject: (error: any) => void }[] = []; const processQueue = (error: any, token: string | null = null) => { failedQueue.forEach(prom => { error ? prom.reject(error) : prom.resolve(token!); }); failedQueue = []; }; // 请求拦截器 api.interceptors.request.use(async (config) => { const token = localStorage.getItem('accessToken'); if (!token) return config; // 解析token获取过期时间 const decoded = JSON.parse(atob(token.split('.')[1])); const expiresAt = decoded.exp * 1000; // 剩余时间小于1分钟则刷新token if (expiresAt - Date.now() < 60 * 1000) { if (!isRefreshing) { isRefreshing = true; try { const res = await axios.post('/api/refresh'); const newToken = res.data.accessToken; localStorage.setItem('accessToken', newToken); config.headers['Authorization'] = `Bearer ${newToken}`; processQueue(null, newToken); } catch (err) { processQueue(err); localStorage.removeItem('accessToken'); window.location.href = '/login'; } finally { isRefreshing = false; } } else { // 等待刷新完成后再继续请求 return new Promise((resolve, reject) => { failedQueue.push({ resolve, reject }); }).then(newToken => { config.headers['Authorization'] = `Bearer ${newToken}`; return config; }); } } else { config.headers['Authorization'] = `Bearer ${token}`; } return config; }); // 响应拦截器处理401错误 api.interceptors.response.use(res => res, async (error) => { const originalRequest = error.config; if (error.response.status === 401 && !originalRequest._retry) { originalRequest._retry = true; try { const res = await axios.post('/api/refresh'); const newToken = res.data.accessToken; localStorage.setItem('accessToken', newToken); originalRequest.headers['Authorization'] = `Bearer ${newToken}`; return api(originalRequest); } catch (err) { localStorage.removeItem('accessToken'); window.location.href = '/login'; return Promise.reject(err); } } return Promise.reject(error); }); export default api;
方案二:定时器定时刷新
适合对实时性要求不高的场景,需注意组件卸载时清理定时器:
import { useEffect } from 'react'; import { useAuth } from './AuthContext'; const useTokenRefresh = () => { const { accessToken, login } = useAuth(); useEffect(() => { if (!accessToken) return; const decoded = JSON.parse(atob(accessToken.split('.')[1])); // 提前30秒刷新token const refreshTime = decoded.exp * 1000 - Date.now() - 30 * 1000; const timer = setTimeout(async () => { try { const res = await fetch('/api/refresh', { method: 'POST' }); if (res.ok) { const data = await res.json(); login(data.accessToken); } else { throw new Error('刷新失败'); } } catch (err) { useAuth().logout(); } }, refreshTime); return () => clearTimeout(timer); }, [accessToken, login]); }; export default useTokenRefresh;
3. 受保护路由实现
结合React Router和AuthContext,实现未登录时重定向到登录页:
import { Navigate, Outlet } from 'react-router-dom'; import { useAuth } from './AuthContext'; const ProtectedRoute = () => { const { accessToken } = useAuth(); if (!accessToken) { return <Navigate to="/login" replace />; } // 前端校验token是否过期 const decoded = JSON.parse(atob(accessToken.split('.')[1])); if (decoded.exp * 1000 < Date.now()) { useAuth().logout(); return <Navigate to="/login" replace />; } return <Outlet />; }; export default ProtectedRoute;
4. 数据库请求优化建议
- 缓存角色信息:后端用Redis缓存用户ID对应的角色列表,设置1小时左右的有效期,避免每次刷新都查询数据库。
- 角色信息存入Refresh Token:若用户角色不频繁变更,可将角色列表直接写入Refresh Token的payload中,刷新时无需查询数据库,直接解析Refresh Token即可获取角色。
内容的提问来源于stack exchange,提问作者yzkael
相关产品推荐
相关产品推荐

