我正在开发一个 React 应用程序,该应用程序需要使用访问令牌和刷新令牌进行 JWT 身份验证。我想以一种防止刷新令牌的多个请求并顺利处理令牌过期的方式来管理身份验证过程。此外,我希望集中警报系统来处理整个应用程序中的成功、错误和信息消息。 ** 我想要实现的目标:**
JWT 身份验证:
在 React 应用程序中正确处理访问和刷新令牌。 当访问令牌过期时,我想使用刷新令牌刷新它,但不会重复服务器请求或导致竞争条件。
刷新访问令牌后自动重试原始请求。 集中警报系统:
我需要一个警报系统,可以处理和显示来自 API 响应(嵌套和非嵌套)的错误消息。 警报系统应该在所有组件中全局可用。 应根据不同的响应(错误、成功等)动态触发警报。
问题: 我使用 Djoser 进行后端处理,使用 React 进行前端处理 我对如何管理刷新令牌机制而不重复调用服务器或引起竞争条件感到困惑。 我还尝试实现一个集中式警报系统,该系统可在全球范围内处理来自服务器的所有响应,包括以结构化方式处理不同类型的错误。
这是我用于 axios 实例的代码,并且对如何有效处理刷新令牌感到困惑
import axios from 'axios';
import { triggerAlertExternally } from '../context/AlertContext';
// Base URL for your API
const API_URL = 'http://127.0.0.1:8000/api';
// Create Axios instance
const axiosInstance = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json',
},
});
// Axios instance for managing token refresh
const refreshTokenAxios = axios.create({
baseURL: API_URL,
headers: {
'Content-Type': 'application/json',
},
});
// Keep track of the refresh process to avoid race conditions
let isRefreshing = false;
let failedRequestsQueue = [];
// Function to add access token to the request
const addAccessTokenToRequest = (config) => {
const accessToken = localStorage.getItem('accessToken');
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
};
// Add request interceptor for adding Authorization header
axiosInstance.interceptors.request.use(addAccessTokenToRequest, (error) => Promise.reject(error));
// Refresh token handling logic
const refreshAccessToken = async () => {
try {
const refreshToken = localStorage.getItem('refreshToken');
if (!refreshToken) throw new Error('No refresh token found, logging out...');
const response = await refreshTokenAxios.post('/auth/jwt/refresh/', {
refresh: refreshToken,
});
const { access, refresh } = response.data;
localStorage.setItem('accessToken', access);
localStorage.setItem('refreshToken', refresh);
return access; // Return the new access token
} catch (error) {
triggerAlertExternally('Session expired. Please log in again.', 'error');
logoutUser();
throw error; // Ensure the error is thrown for further handling
}
};
// Handle token refresh and retry the original request
const retryFailedRequests = (accessToken) => {
failedRequestsQueue.forEach(({ resolve }) => resolve(accessToken));
failedRequestsQueue = []; // Clear the queue after retrying
};
// Add response interceptor for handling 401 errors
axiosInstance.interceptors.response.use(
(response) => response, // Return successful responses as is
async (error) => {
const originalRequest = error.config;
// If the error is 401 and it's not a refresh request
if (error.response?.status === 401 && !originalRequest._retry) {
// Prevent multiple refresh token requests (race condition)
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedRequestsQueue.push({ resolve, reject });
});
}
// Mark this request as having a retry to avoid infinite loops
originalRequest._retry = true;
isRefreshing = true;
try {
const accessToken = await refreshAccessToken();
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
retryFailedRequests(accessToken); // Retry the failed requests with the new token
return axiosInstance(originalRequest); // Retry the original request with the new token
} catch (err) {
return Promise.reject(err); // Handle any errors if token refresh fails
} finally {
isRefreshing = false; // Reset the refreshing flag
}
}
// Handle non-401 errors (or if refresh fails)
return Promise.reject(error);
}
);
// Logout user and clear all session data
function logoutUser() {
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
localStorage.removeItem('user'); // Clear user data from localStorage
// window.location.href = '/login'; // Redirect to login page
}
export default axiosInstance;
这是警报系统,我的方法是直接从组件获取错误消息,然后 显示并从响应中正确提取它
import React, { createContext, useState, useContext, useEffect } from 'react';
import { Snackbar, Alert } from '@mui/material';
import { Error as ErrorIcon, CheckCircle as SuccessIcon, Info as InfoIcon } from '@mui/icons-material';
// Create a context for the alert system
const AlertContext = createContext();
// Global reference to the showAlert function for external access
let externalShowAlert = null;
// Function to expose the showAlert globally
export const setExternalShowAlert = (showAlertFunc) => {
externalShowAlert = showAlertFunc;
};
// Function to trigger alert externally
export const triggerAlertExternally = (message, severity = 'error', statusCode = 500) => {
if (externalShowAlert) {
externalShowAlert(message, severity, statusCode);
}
};
// Helper function to format error messages for Djoser responses
const formatErrorMessage = (error) => {
if (typeof error === 'string') {
// Direct string error message
return `${error}`;
}
if (Array.isArray(error)) {
// Error array: Join them with a separator
return error.join(' ');
}
if (typeof error === 'object') {
// Nested error handling
const formattedMessages = [];
Object.keys(error).forEach((key) => {
const value = error[key];
if (Array.isArray(value)) {
// If the value is an array, join and display
formattedMessages.push(`${key}: ${value.join(', ')}`);
} else if (typeof value === 'object') {
// If the value is an object, recursively format it
formattedMessages.push(`${key}: ${formatErrorMessage(value)}`);
}
});
return formattedMessages.join(' ');
}
return 'An unexpected error occurred'; // Default fallback
};
// Define custom icons for each alert type
const getAlertIcon = (severity) => {
switch (severity) {
case 'success':
return <SuccessIcon style={{ color: 'green' }} />;
case 'info':
return <InfoIcon style={{ color: 'blue' }} />;
case 'error':
default:
return <ErrorIcon style={{ color: 'red' }} />;
}
};
// AlertProvider component
export const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null);
const [statusCode, setStatusCode] = useState(null);
// Function to display the alert
const showAlert = (message, severity = 'error', statusCode = 500) => {
const formattedMessage = formatErrorMessage(message);
setAlert({ message: formattedMessage, severity });
setStatusCode(statusCode);
setTimeout(() => setAlert(null), 5000); // Hide alert after 5 seconds
};
// Set the global showAlert function when the component mounts
useEffect(() => {
setExternalShowAlert(showAlert);
}, [showAlert]);
return (
<AlertContext.Provider value={{ showAlert }}>
{children}
{alert && (
<Snackbar open={Boolean(alert)} autoHideDuration={5000}>
<Alert
severity={alert.severity}
sx={{ width: '100%' }}
icon={getAlertIcon(alert.severity)} // Dynamically set the icon based on severity
>
{statusCode && <strong>Status {statusCode} - </strong>}
{alert.message}
</Alert>
</Snackbar>
)}
</AlertContext.Provider>
);
};
// Custom hook to use the alert context
export const useAlert = () => useContext(AlertContext);
关于管理JTW。
我最近在 React 客户端应用程序中实现了类似的流程,该应用程序使用访问/刷新令牌对处理身份验证。您可以在这里查看:https://github.com/zavvdev/fe-infra。
基本上,我已经用 axios + axios-retry 流程完成了,其中:
invalid_token
响应的请求都应该重试(但有特定的时间以防止无限重试)。还包括对突变请求的重试,但仅限于此类请求。有关更多详细信息,请查看上面的存储库。希望有帮助。