You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React应用因Axios拦截器会话处理问题导致用户频繁登出求助

React应用频繁登出问题:Axios令牌刷新拦截器优化方案

问题描述

我正在开发一个React前端应用,目前遇到用户频繁登出的严重问题,推测问题出在会话处理环节,尤其是用于令牌刷新的Axios拦截器实现上。

背景信息

技术栈

  • 前端框架:React
  • API调用工具:Axios
  • 认证方式:JWT(JSON Web Tokens)
  • 令牌存储:localStorage

预期目标

  1. 基于Axios拦截器实现可靠的令牌刷新机制
  2. 确保用户维持认证会话,避免频繁登出
  3. 在令牌过期时优雅地触发刷新操作

当前代码

import { useState, useEffect } from 'react'
import axios from 'axios'

const apiUrl = process.env.NEXT_PUBLIC_MY_API_URL

export function useApi(token) {
  //const [token, setToken] = useLocalStorage('token')

  const api = axios.create({
    baseURL: apiUrl,
    timeout: 100000,// 100秒
    withCredentials: true,
    'Access-Control-Allow-Credentials': true
  })
  
  api.interceptors.request.use(
    config => {
      const customizedConfig = config

      if (token) {
        customizedConfig.headers = { ...config.headers, ...{ Authorization: `Bearer ${token}` } }
      } else {
        Promise.reject('Not authorized')
      }

      return customizedConfig
    },
    error => Promise.reject(error)
  )

  api.interceptors.response.use(
    (response) => response,
    (err) => {
      if (err.response && err.response.status === 403) {
        console.log('Fetching refresh_token...');
        const data = {
          refreshToken: localStorage.getItem('token')
        }
  
        return axios.post(`${apiUrl}token/refresh`, data).then((token) => {
            // Retry original request with new token
            console.log('refreh', token)
            const { config } = err;
            localStorage.setItem("token", token.data.jwt);
            config.headers.Authorization = `Bearer ${token.data.jwt}`;
            
            return new Promise((resolve, reject) => {
              axios
                .request(config)
                .then((response) => {
                  resolve(response);
                })
                .catch((error) => {
                  reject(error);
                });
            });
          })
          .catch((error) => {
            console.error(error)
            console.log('403 error.No new token received. Removing token from localStorage.');
            localStorage.removeItem('token');
            console.log(error);  
          });

        // maybe redirect to /login if needed !
      }
      return new Promise((resolve, reject) => {
        reject(err);
      });
    },
  );

  api.interceptors.response.use(
    (response) => response,
    (err) => {
      if (err.response && err.response.status === 401) {
        console.log('Fetching refresh_token...');
        const data = {
          refreshToken: localStorage.getItem('token')
        }
  
        return axios.post(`${apiUrl}token/refresh`, data).then((token) => {
            // Retry original request with new token
            console.log('refreh', token)
            const { config } = err;
            localStorage.setItem("token", token.data.jwt);
            config.headers.Authorization = `Bearer ${token.data.jwt}`;
            
            return new Promise((resolve, reject) => {
              axios
                .request(config)
                .then((response) => {
                  resolve(response);
                })
                .catch((error) => {
                  reject(error);
                });
            });
          })
          .catch((error) => {
            console.log('401 error. No new token received. Removing token from localStorage.');
            localStorage.removeItem('token');
            console.log(error);  
          });

        // maybe redirect to /login if needed !
      }
      return new Promise((resolve, reject) => {
        reject(err);
      });
    },
  );

  return api
}

问题分析

当前代码存在几个核心问题导致频繁登出:

  1. 重复添加响应拦截器:两次调用api.interceptors.response.use,后添加的拦截器会覆盖前一个的错误处理逻辑,导致403错误无法被正确捕获处理。
  2. 令牌同步问题:useApi接收的token参数与localStorage中的令牌不同步,当刷新令牌后,组件中的token状态未更新,后续请求仍使用旧令牌。
  3. 刷新请求重复触发:当多个请求同时返回401/403时,会发起多次刷新令牌请求,浪费资源且可能导致状态混乱。
  4. 错误处理不完整:刷新令牌失败后未执行跳转登录逻辑,用户停留在未授权状态。

优化后的解决方案

import axios from 'axios'

const apiUrl = process.env.NEXT_PUBLIC_MY_API_URL

// 创建单例Axios实例,避免重复创建
const api = axios.create({
  baseURL: apiUrl,
  timeout: 100000,
  withCredentials: true
})

// 标记是否正在刷新令牌,防止重复请求
let isRefreshing = false
// 存储等待刷新令牌的请求队列
let requestQueue = []

// 从localStorage获取当前令牌
const getCurrentToken = () => localStorage.getItem('token')

// 更新令牌并处理队列中的请求
const updateTokenAndRetry = (newToken) => {
  localStorage.setItem('token', newToken)
  requestQueue.forEach(callback => callback(newToken))
  requestQueue = []
}

// 请求拦截器:自动添加Authorization头
api.interceptors.request.use(
  (config) => {
    const token = getCurrentToken()
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => Promise.reject(error)
)

// 响应拦截器:统一处理令牌过期
api.interceptors.response.use(
  (response) => response,
  async (err) => {
    const originalConfig = err.config

    // 排除刷新令牌本身的请求,避免死循环
    if ((err.response.status === 401 || err.response.status === 403) && !originalConfig._retry) {
      originalConfig._retry = true

      if (isRefreshing) {
        // 若正在刷新,将当前请求加入队列等待
        return new Promise((resolve) => {
          requestQueue.push((newToken) => {
            originalConfig.headers.Authorization = `Bearer ${newToken}`
            resolve(api(originalConfig))
          })
        })
      }

      isRefreshing = true

      try {
        // 发起刷新令牌请求
        const refreshToken = getCurrentToken()
        const response = await axios.post(`${apiUrl}token/refresh`, { refreshToken })
        const newToken = response.data.jwt

        updateTokenAndRetry(newToken)
        // 重试原请求
        originalConfig.headers.Authorization = `Bearer ${newToken}`
        return api(originalConfig)
      } catch (refreshErr) {
        // 刷新失败,清除令牌并跳转登录
        localStorage.removeItem('token')
        window.location.href = '/login'
        return Promise.reject(refreshErr)
      } finally {
        isRefreshing = false
      }
    }

    return Promise.reject(err)
  }
)

// 自定义Hook,供组件使用
export function useApi() {
  return api
}

优化说明

  1. 单例Axios实例:全局创建一个Axios实例,避免每次调用useApi都创建新实例,确保拦截器只注册一次。
  2. 防止重复刷新:用isRefreshing标记和请求队列,确保同一时间只有一个刷新请求,其他请求等待令牌更新后重试。
  3. 令牌同步:直接从localStorage获取令牌,避免组件状态与存储不同步的问题;刷新令牌后更新存储并通知队列中的请求。
  4. 统一错误处理:合并401/403的处理逻辑,避免重复代码;刷新失败后自动跳转登录,提升用户体验。
  5. 避免死循环:给原请求添加_retry标记,防止刷新令牌失败后重复触发拦截器。

内容的提问来源于stack exchange,提问作者Michele Fabiani

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 03:03:10