如何在 React 中高效管理 JWT 身份验证(刷新和访问令牌)而无需重复服务器调用并实现集中式警报系统

问题描述 投票:0回答:1

我正在开发一个 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);

reactjs django django-rest-framework axios djoser
1个回答
0
投票

关于管理JTW。

我最近在 React 客户端应用程序中实现了类似的流程,该应用程序使用访问/刷新令牌对处理身份验证。您可以在这里查看:https://github.com/zavvdev/fe-infra

基本上,我已经用 axios + axios-retry 流程完成了,其中:

  1. 每个返回
    invalid_token
    响应的请求都应该重试(但有特定的时间以防止无限重试)。还包括对突变请求的重试,但仅限于此类请求。
  2. 重试请求时,刷新令牌在特定时间内仅触发一次,以防止多次刷新令牌请求。
  3. 成功刷新后 - 如果在指定的重试时间范围内成功完成刷新,则将使用新令牌重试所有先前失败的请求。

有关更多详细信息,请查看上面的存储库。希望有帮助。

最新问题
© www.soinside.com 2019 - 2025. All rights reserved.