如何在Next.js的Axios拦截器中处理Auth0令牌刷新失败后的用户登出
以下是针对你的需求优化后的Axios拦截器代码,重点解决令牌刷新失败时的自动登出与重定向问题,并附带最佳实践说明:
完整优化代码
import { AxiosInstance, isAxiosError } from 'axios'; import { Auth0Client } from '@auth0/auth0-react'; // 全局状态:控制令牌刷新并发,避免重复请求 let isAlreadyFetchingAccessToken = false; // 订阅者队列:处理同时触发的多个请求 let subscribers: ((accessToken: string) => void)[] = []; /** * 添加请求订阅者,等待新令牌获取后重试 */ function addSubscriber(callback: (accessToken: string) => void) { subscribers.push(callback); } /** * 令牌刷新完成后通知所有订阅者 */ function onAccessTokenFetched(accessToken: string) { subscribers.forEach(callback => callback(accessToken)); subscribers = []; } /** * 调用Auth0刷新令牌接口 */ async function refreshAccessToken() { try { const { data } = await axios.get(REFRESH_ACCESS_TOKEN_API); return data.access_token; } catch (error) { // 抛出错误,让拦截器处理登出逻辑 throw error; } } /** * 配置Axios响应拦截器 * @param axiosInstance Axios实例 * @param auth0 Auth0客户端实例(从@auth0/auth0-react获取) */ export function setupResponseInterceptor(axiosInstance: AxiosInstance, auth0: Auth0Client) { axiosInstance.interceptors.response.use( response => response, async error => { const originalRequest = error.config; // 防止无限循环:标记已重试过的请求,不再处理 if (originalRequest._retry) { throw error; } // 仅处理令牌过期导致的401错误 if (isAxiosError(error) && error.response?.status === 401) { const isTokenExpired = error.response.data?.error === 'invalid_token' || error.response.data?.error_description?.includes('expired'); // 非令牌过期的401(如权限不足)直接抛出错误 if (!isTokenExpired) { throw error; } if (!isAlreadyFetchingAccessToken) { isAlreadyFetchingAccessToken = true; try { const newAccessToken = await refreshAccessToken(); // 更新Auth0会话中的令牌 await auth0.setSession({ access_token: newAccessToken }); // 通知所有等待的请求重试 onAccessTokenFetched(newAccessToken); } catch (err) { // 刷新失败:执行Auth0登出并重定向到首页 await auth0.logout({ logoutParams: { returnTo: window.location.origin } }); // 清空订阅者队列,避免后续请求无效等待 subscribers = []; return Promise.reject(err); } finally { // 重置刷新状态,允许后续请求触发刷新 isAlreadyFetchingAccessToken = false; } } // 将当前请求加入订阅队列,等待新令牌 return new Promise((resolve) => { addSubscriber((accessToken) => { originalRequest._retry = true; originalRequest.headers.Authorization = `Bearer ${accessToken}`; resolve(axiosInstance(originalRequest)); }); }); } // 非401错误直接抛出 throw error; }, ); }
关键逻辑说明
Auth0客户端注入
拦截器需要接收Auth0的客户端实例(通过useAuth0钩子在客户端组件中获取),这样才能调用官方的logout和setSession方法,确保登出逻辑符合Auth0的会话管理规范。避免无限循环
给原始请求添加_retry标记,当请求重试后仍失败时,拦截器会直接抛出错误,不会重复进入刷新流程。精准处理令牌过期
通过判断响应的错误信息(如invalid_token或包含expired的描述),只处理令牌过期导致的401,避免误处理权限不足等其他场景的401错误。刷新失败的登出流程
当refreshAccessToken抛出错误时,立即调用auth0.logout,指定重定向到首页(window.location.origin),同时清空订阅者队列,重置刷新状态,避免无效资源占用。并发请求管理
使用订阅者模式处理多个同时触发的请求,确保所有请求都能在令牌刷新完成后,使用新令牌重试。
最佳实践
客户端与服务器端区分处理
如果你的Axios拦截器用于Next.js的服务器端代码(如API路由、getServerSideProps),需要使用@auth0/nextjs-auth0提供的destroySession方法处理登出,而不是客户端的logout。添加错误日志
在登出前可以记录刷新失败的错误信息(如错误栈、请求URL),方便后续排查问题。用户友好提示
登出前可以通过全局状态或Toast组件给用户显示提示(如"会话已过期,请重新登录"),提升用户体验。令牌存储同步
刷新令牌后,务必调用auth0.setSession更新Auth0的会话存储,确保其他组件也能获取到新令牌。
内容的提问来源于stack exchange,提问作者Niraj Shrestha

