React中JWT认证(访问/刷新令牌)与全局告警系统实现问询
React + Djoser JWT认证优化与全局告警实现方案
一、JWT令牌刷新机制优化(解决重复请求、竞态条件、自动重试)
核心思路是通过状态锁+请求队列控制刷新流程,避免重复发起刷新请求,同时将等待中的请求暂存,刷新成功后批量重试。
1. 核心实现逻辑
- 维护
isRefreshing标记,防止并发刷新请求 - 用
failedRequestsQueue队列暂存等待刷新完成的请求 - 在Axios响应拦截器中捕获401 token过期错误,触发刷新流程
- 刷新成功后更新token,重试队列和原请求;失败则清空token并跳转登录
2. 代码实现(Axios实例配置)
// axiosInstance.js import axios from 'axios'; const axiosInstance = axios.create({ baseURL: '/api', }); let isRefreshing = false; let failedRequestsQueue = []; // 请求拦截器:自动携带access token axiosInstance.interceptors.request.use( (config) => { const accessToken = localStorage.getItem('access'); if (accessToken) { config.headers.Authorization = `Bearer ${accessToken}`; } return config; }, (error) => Promise.reject(error) ); // 响应拦截器:处理token过期 axiosInstance.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; // 判定为token过期的401错误(匹配Djoser返回的错误特征) if (error.response?.status === 401 && error.response.data.detail?.includes('token') && !originalRequest._retry) { originalRequest._retry = true; // 标记已重试,避免循环 if (isRefreshing) { // 正在刷新,将请求加入队列等待 return new Promise((resolve, reject) => { failedRequestsQueue.push({ resolve, reject }); }).then((token) => { originalRequest.headers.Authorization = `Bearer ${token}`; return axiosInstance(originalRequest); }).catch((err) => Promise.reject(err)); } isRefreshing = true; try { // 调用Djoser的refresh接口 const refreshToken = localStorage.getItem('refresh'); const refreshRes = await axiosInstance.post('/auth/jwt/refresh/', { refresh: refreshToken }); const newAccessToken = refreshRes.data.access; // 更新本地token localStorage.setItem('access', newAccessToken); // 更新当前请求的token originalRequest.headers.Authorization = `Bearer ${newAccessToken}`; // 重试队列中所有等待的请求 failedRequestsQueue.forEach(({ resolve }) => resolve(newAccessToken)); failedRequestsQueue = []; // 重试原请求 return axiosInstance(originalRequest); } catch (refreshError) { // 刷新失败:清空token,跳转登录页 localStorage.removeItem('access'); localStorage.removeItem('refresh'); window.location.href = '/login'; // 拒绝队列中所有请求 failedRequestsQueue.forEach(({ reject }) => reject(refreshError)); failedRequestsQueue = []; return Promise.reject(refreshError); } finally { isRefreshing = false; } } return Promise.reject(error); } ); export default axiosInstance;
二、全局告警系统(结构化处理嵌套/非嵌套错误)
通过React Context实现全局告警能力,自动解析API返回的不同格式错误,支持全组件调用。
1. 核心实现逻辑
- 用Context封装告警状态和操作方法
- 区分处理三种常见API返回格式:
- 非嵌套错误:
{detail: "错误信息"} - 嵌套字段错误:
{errors: {email: ["格式错误"]}} - 成功消息:
{message: "操作成功"}
- 非嵌套错误:
- 支持自动关闭告警,提供手动关闭入口
2. 代码实现
告警Context与Provider
// AlertContext.jsx import { createContext, useContext, useState } from 'react'; const AlertContext = createContext(); export const AlertProvider = ({ children }) => { const [alerts, setAlerts] = useState([]); // 添加告警:支持success/error/warning三种类型 const addAlert = ({ type = 'error', message }) => { const id = Date.now(); setAlerts(prev => [...prev, { id, type, message }]); // 5秒后自动关闭告警(可配置) setTimeout(() => removeAlert(id), 5000); }; const removeAlert = (id) => { setAlerts(prev => prev.filter(alert => alert.id !== id)); }; // 结构化处理API错误 const handleApiError = (error) => { let errorMessage = '请求失败,请稍后重试'; if (error.response?.data) { const data = error.response.data; // 处理Djoser表单嵌套错误 if (data.errors) { errorMessage = Object.values(data.errors).flat().join('\n'); } // 处理非嵌套错误(如token过期、权限不足) else if (data.detail) { errorMessage = data.detail; } // 处理字符串类型错误 else if (typeof data === 'string') { errorMessage = data; } } addAlert({ type: 'error', message: errorMessage }); }; // 处理成功消息 const handleSuccessMessage = (message) => { addAlert({ type: 'success', message }); }; return ( <AlertContext.Provider value={{ addAlert, removeAlert, handleApiError, handleSuccessMessage }}> {children} </AlertContext.Provider> ); }; export const useAlert = () => useContext(AlertContext);
告警展示组件
// AlertComponent.jsx import { useAlert } from './AlertContext'; const AlertComponent = () => { const { alerts, removeAlert } = useAlert(); return ( <div className="fixed top-4 right-4 z-50 space-y-2"> {alerts.map(alert => ( <div key={alert.id} className={`px-4 py-3 rounded-lg ${ alert.type === 'success' ? 'bg-green-100 text-green-800' : alert.type === 'error' ? 'bg-red-100 text-red-800' : 'bg-yellow-100 text-yellow-800' } flex justify-between items-center`} > <span>{alert.message}</span> <button onClick={() => removeAlert(alert.id)} className="ml-4 text-xl font-bold hover:opacity-70" > × </button> </div> ))} </div> ); }; export default AlertComponent;
3. 全局使用示例
// App.jsx import { AlertProvider } from './AlertContext'; import AlertComponent from './AlertComponent'; import axiosInstance from './axiosInstance'; import { useAlert } from './AlertContext'; // 业务组件中调用示例 const UserProfile = () => { const { handleApiError, handleSuccessMessage } = useAlert(); const updateProfile = async (formData) => { try { await axiosInstance.put('/auth/users/me/', formData); handleSuccessMessage('个人信息更新成功'); } catch (error) { handleApiError(error); } }; return ( <form onSubmit={(e) => { e.preventDefault(); updateProfile(new FormData(e.target)); }}> {/* 表单内容 */} <button type="submit">保存修改</button> </form> ); }; function App() { return ( <AlertProvider> <AlertComponent /> <UserProfile /> {/* 其他业务组件 */} </AlertProvider> ); } export default App;
内容的提问来源于stack exchange,提问作者pravin bansode
相关产品推荐
相关产品推荐

