React Axios响应拦截器中Refresh Token过期如何触发自定义弹窗后跳转登录
React Context环境下axios拦截器触发自定义会话过期弹窗方案
核心思路
axios拦截器属于普通JS模块,无法直接调用React组件内的Context方法,所以需要搭建一个跨模块的通信通道,让拦截器可以发送会话过期的信号,Context层接收信号后控制自定义弹窗的显示。
具体实现步骤
1. 改造Auth Context Provider
首先在你的自定义鉴权Context中新增弹窗控制逻辑,同时内置自定义弹窗组件:
import { createContext, useContext, useState, useEffect } from 'react' import { useHistory } from 'react-router-dom' import CustomModal from './你的自定义弹窗组件路径' const AuthContext = createContext() export const AuthProvider = ({ children }) => { const history = useHistory() const [showExpiredModal, setShowExpiredModal] = useState(false) // 加锁避免重复触发弹窗 const [modalLocked, setModalLocked] = useState(false) // 弹窗确认后的回调 const handleConfirm = () => { setShowExpiredModal(false) setModalLocked(false) // 这里执行你原来的清空存储+跳转逻辑 removeLocalStorageData() history.replace('/login') } // 对外暴露的触发弹窗方法 const showSessionExpiredModal = () => { if (modalLocked) return setShowExpiredModal(true) setModalLocked(true) } // 方案A:用全局事件总线接收信号的逻辑,后面会讲 useEffect(() => { const handleExpired = () => showSessionExpiredModal() window.eventBus.on('sessionExpired', handleExpired) return () => window.eventBus.off('sessionExpired', handleExpired) }, []) // 方案B:挂载到全局window的简化方案,不需要事件总线就用这个 // useEffect(() => { // window.$auth = { showSessionExpiredModal } // return () => delete window.$auth // }, []) return ( <AuthContext.Provider value={{ showSessionExpiredModal /* 其他你原来的context值 */ }}> {children} {/* 自定义弹窗直接放在Provider里,全局复用 */} <CustomModal visible={showExpiredModal} title="提示" content="会话已过期,请重新登录" onConfirm={handleConfirm} maskClosable={false} // 禁止点击蒙层关闭,必须点确认 /> </AuthContext.Provider> ) } export const useAuth = () => useContext(AuthContext)
2. 实现跨模块通信通道
方案A:极简事件总线(推荐,无全局污染)
新建eventBus.js文件,实现基础的订阅发布能力:
class EventBus { constructor() { this.events = {} } on(eventName, callback) { if (!this.events[eventName]) this.events[eventName] = [] this.events[eventName].push(callback) } emit(eventName, ...args) { if (this.events[eventName]) { this.events[eventName].forEach(cb => cb(...args)) } } off(eventName, callback) { if (this.events[eventName]) { this.events[eventName] = this.events[eventName].filter(cb => cb !== callback) } } } window.eventBus = new EventBus()
在项目入口文件index.js里引入这个文件,确保全局初始化。
方案B:全局挂载(适合小型项目,代码量最少)
不需要额外新建文件,直接用上面Context代码里注释的挂载到window的逻辑即可。
3. 修改原有axios拦截器逻辑
把你原来doRefreshToken函数里直接清空存储和刷新页面的逻辑,替换成发通信信号即可:
export const doRefreshToken = async (err, API) => { const originalConfig = err.config; if (err.response) { if (err.response.status === 401 && !originalConfig._retry) { originalConfig._retry = true; try { const rs = await refreshTokenAPI(); const { token, refreshToken } = rs.data; setToken(token); setRefreshToken(refreshToken); return API(originalConfig); } catch (_error) { // 删掉原来的removeLocalStorageData和window.location.reload // 触发会话过期信号 window.eventBus.emit('sessionExpired') // 如果用方案B全局挂载,就写window.$auth?.showSessionExpiredModal() return Promise.reject(_error); } } if (err.response.status === 403 && err.response.data && originalConfig.url === '/refresh-token') { // 同样删掉原来的清空和刷新逻辑,触发信号 window.eventBus.emit('sessionExpired') // 如果用方案B就写window.$auth?.showSessionExpiredModal() } if (err.response.status === 403 && err.response.data) { return Promise.reject(err.response.data); } } }
注意事项
- 弹窗要设置
maskClosable=false且不显示取消按钮,避免用户跳过登录继续操作 - 加弹窗锁是为了避免多个请求同时过期时重复触发多次弹窗
- 所有清空存储、路由跳转的逻辑都放在弹窗的确认回调里执行,确保用户看到提示后才跳转
内容的提问来源于stack exchange,提问作者Smith Dwayne
相关产品推荐
相关产品推荐

