React应用因Axios拦截器会话处理问题导致用户频繁登出求助
React应用频繁登出问题:Axios令牌刷新拦截器优化方案
问题描述
我正在开发一个React前端应用,目前遇到用户频繁登出的严重问题,推测问题出在会话处理环节,尤其是用于令牌刷新的Axios拦截器实现上。
背景信息
技术栈
- 前端框架:React
- API调用工具:Axios
- 认证方式:JWT(JSON Web Tokens)
- 令牌存储:localStorage
预期目标
- 基于Axios拦截器实现可靠的令牌刷新机制
- 确保用户维持认证会话,避免频繁登出
- 在令牌过期时优雅地触发刷新操作
当前代码
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 }
问题分析
当前代码存在几个核心问题导致频繁登出:
- 重复添加响应拦截器:两次调用
api.interceptors.response.use,后添加的拦截器会覆盖前一个的错误处理逻辑,导致403错误无法被正确捕获处理。 - 令牌同步问题:
useApi接收的token参数与localStorage中的令牌不同步,当刷新令牌后,组件中的token状态未更新,后续请求仍使用旧令牌。 - 刷新请求重复触发:当多个请求同时返回401/403时,会发起多次刷新令牌请求,浪费资源且可能导致状态混乱。
- 错误处理不完整:刷新令牌失败后未执行跳转登录逻辑,用户停留在未授权状态。
优化后的解决方案
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 }
优化说明
- 单例Axios实例:全局创建一个Axios实例,避免每次调用
useApi都创建新实例,确保拦截器只注册一次。 - 防止重复刷新:用
isRefreshing标记和请求队列,确保同一时间只有一个刷新请求,其他请求等待令牌更新后重试。 - 令牌同步:直接从
localStorage获取令牌,避免组件状态与存储不同步的问题;刷新令牌后更新存储并通知队列中的请求。 - 统一错误处理:合并401/403的处理逻辑,避免重复代码;刷新失败后自动跳转登录,提升用户体验。
- 避免死循环:给原请求添加
_retry标记,防止刷新令牌失败后重复触发拦截器。
内容的提问来源于stack exchange,提问作者Michele Fabiani
相关产品推荐
相关产品推荐

