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

React刷新令牌过期时无法自动登出并重定向至登录页问题

React Token刷新失败后无法立即登出的问题修复

问题概述

系统采用Token Refresh与Token Rotation机制,令牌过期时后端自动删除Cookie,前端需要同步删除localStorage中的令牌并将用户重定向到登录页。目前用Axios拦截器检测403状态码调用/refresh端点刷新令牌,但刷新失败(refresh token过期)时,无法立即删除令牌、跳转登录页,需要2-3次页面刷新才生效,期间无法加载数据,影响体验。

代码片段

axiosPrivate.js

import { setIsAuthenticated } from '../features/auth/authSlice';
import instance from "./axiosConfig";
import { memoizedRefreshToken } from "./axiosRefreshToken";

instance.interceptors.request.use(
    async (config) => {
        const authenticatedUser = JSON.parse(localStorage.getItem("authenticatedUser"));
    
        if (authenticatedUser?.accessToken) {
            config.headers = {
                ...config.headers,
                authorization: `Bearer ${authenticatedUser?.accessToken}`,
            };
        }
    
        return config;
    },
    (error) => Promise.reject(error)
);

instance.interceptors.response.use(
    (response) => response,
    async (error) => {
        const config = error?.config;
    
        if (error?.response?.status === 403 && !config?.sent) {
            config.sent = true;
            console.log("Inside If: ", config);
    
            const result = await memoizedRefreshToken();
    
            if (result?.accessToken) {
                console.log("Access Token Returned: ", result)
                config.headers = {
                    ...config.headers,
                    authorization: `Bearer ${result?.accessToken}`,
                };
            } else {
                console.log("No Access Token ")
                store.dispatch(setIsAuthenticated(false));
            }
    
            return instance(config);
        }
        console.log("Outside If: ", config);
        store.dispatch(setIsAuthenticated(false));
        return Promise.reject(error);
    }
);

export const axiosPrivate = instance;

axiosRefreshToken.js

import { store } from '../features/store';
import { setIsAuthenticated } from '../features/auth/authSlice';
import instance from "./axiosConfig";

const refreshTokenFn = async () => {
    try {
        const response = await instance.get("/auth/refresh");

        const authenticatedUser = response.data;

        if (!authenticatedUser?.accessToken) {
            localStorage.removeItem("authenticatedUser");
            store.dispatch(setIsAuthenticated(false));
        }

        localStorage.setItem("authenticatedUser", JSON.stringify(authenticatedUser));
        store.dispatch(setIsAuthenticated(true));

        return authenticatedUser;
    } catch (error) {
        localStorage.removeItem("authenticatedUser");
        store.dispatch(setIsAuthenticated(false));
    }
};

const maxAge = 10000;

export const memoizedRefreshToken = mem(refreshTokenFn, {
    maxAge,
});

问题根源

  1. 缓存导致逻辑延迟:memoizedRefreshToken设置了10秒缓存,刷新失败后,失败结果会被缓存10秒,这段时间内的请求都会复用缓存的失败结果,无法触发登出逻辑,直到缓存过期。
  2. 错误分支无返回值:refreshTokenFn的catch块只执行了删令牌和dispatch,但没有返回任何值,导致拦截器里的result是undefined,加上缓存,后续请求一直拿不到明确的失败信号。
  3. 拦截器循环请求:刷新失败后仍返回instance(config)重新发起原请求,此时无有效令牌,会再次触发403,陷入循环,直到缓存过期才会走到外层的登出逻辑。
  4. 缺少重定向逻辑:现有代码只更新了Redux状态,没有触发页面跳转,用户停留在原页面,需要手动刷新才能触发路由守卫的登出逻辑。

修复方案

1. 移除Refresh Token的缓存

刷新失败意味着refresh token已过期,缓存失败结果没有意义,直接移除缓存:

修改axiosRefreshToken.js:

import { store } from '../features/store';
import { setIsAuthenticated } from '../features/auth/authSlice';
import instance from "./axiosConfig";

const refreshTokenFn = async () => {
    try {
        const response = await instance.get("/auth/refresh");
        const authenticatedUser = response.data;

        if (!authenticatedUser?.accessToken) {
            localStorage.removeItem("authenticatedUser");
            store.dispatch(setIsAuthenticated(false));
            return null; // 返回明确的失败标识
        }

        localStorage.setItem("authenticatedUser", JSON.stringify(authenticatedUser));
        store.dispatch(setIsAuthenticated(true));
        return authenticatedUser;
    } catch (error) {
        localStorage.removeItem("authenticatedUser");
        store.dispatch(setIsAuthenticated(false));
        return null; // 返回明确的失败标识
    }
};

// 直接导出非缓存版本
export const refreshToken = refreshTokenFn;

2. 修复Axios拦截器逻辑

当刷新失败时,直接终止原请求,触发登出和重定向:

修改axiosPrivate.js:

import { setIsAuthenticated } from '../features/auth/authSlice';
import instance from "./axiosConfig";
import { refreshToken } from "./axiosRefreshToken";
// 导入自定义路由history实例(需要提前创建,比如在utils/history.js中)
import { history } from '../utils/history';

instance.interceptors.request.use(
    async (config) => {
        const authenticatedUser = JSON.parse(localStorage.getItem("authenticatedUser"));
    
        if (authenticatedUser?.accessToken) {
            config.headers.authorization = `Bearer ${authenticatedUser.accessToken}`;
        }
    
        return config;
    },
    (error) => Promise.reject(error)
);

instance.interceptors.response.use(
    (response) => response,
    async (error) => {
        const config = error?.config;
    
        if (error?.response?.status === 403 && !config?.sent) {
            config.sent = true;
            const result = await refreshToken();
    
            if (result?.accessToken) {
                config.headers.authorization = `Bearer ${result.accessToken}`;
                return instance(config);
            } else {
                // 刷新失败,直接登出跳转
                localStorage.removeItem("authenticatedUser");
                store.dispatch(setIsAuthenticated(false));
                history.push('/login');
                return Promise.reject(new Error('Refresh token expired'));
            }
        }
        // 其他错误或已尝试过刷新的情况,直接登出跳转
        localStorage.removeItem("authenticatedUser");
        store.dispatch(setIsAuthenticated(false));
        history.push('/login');
        return Promise.reject(error);
    }
);

export const axiosPrivate = instance;

3. 补充路由重定向逻辑

如果没有自定义history实例,可以在Redux的authSlice中监听状态变化,自动跳转:

修改authSlice.js:

import { createSlice } from '@reduxjs/toolkit';
import { history } from '../utils/history';

const initialState = {
    isAuthenticated: !!localStorage.getItem('authenticatedUser'),
};

const authSlice = createSlice({
    name: 'auth',
    initialState,
    reducers: {
        setIsAuthenticated: (state, action) => {
            state.isAuthenticated = action.payload;
            if (!action.payload) {
                // 状态变为未认证时,立即跳转登录页
                history.push('/login');
            }
        },
    },
});

export const { setIsAuthenticated } = authSlice.actions;
export default authSlice.reducer;

4. 自定义history实例创建(可选)

在utils/history.js中创建路由history实例:

import { createBrowserHistory } from 'history';

export const history = createBrowserHistory();

如果使用React Router v6,需要在路由配置中使用Router组件而非BrowserRouter,传入自定义history:

import { Router } from 'react-router-dom';
import { history } from './utils/history';

function App() {
  return (
    <Router history={history}>
      {/* 路由配置 */}
    </Router>
  );
}

关键修复点

  • 移除Refresh Token的缓存,避免失败结果被缓存延迟登出逻辑
  • 在refreshToken的错误分支返回明确的失败标识,让拦截器能正确判断
  • 拦截器中刷新失败时直接终止原请求,触发登出和重定向,避免循环请求
  • 添加自动重定向逻辑,确保状态变为未认证时立即跳转登录页

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 02:20:34