基于Axios拦截器从MSAL获取刷新令牌的问题排查
Axios 401错误时从MSAL获取新令牌的问题解决
问题概述
在React项目中使用Axios和MSAL时,遇到两个核心问题:
- Axios请求失败时,错误状态为0或undefined,仅返回“Network Error”,无法准确识别401过期令牌错误;
- 封装Axios的
agent.ts是工具类,无法使用React Hooks,不知道如何调用MSAL的令牌刷新方法。
相关代码
agent.ts
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'; import { ICategory, IIncident } from '../models/ApiInterface'; const instance = axios.create({ baseURL: 'https://localhost:7088', // 修正:补全单引号 headers: { 'Content-Type': 'application/json' }, }); instance.interceptors.request.use( (config: AxiosRequestConfig) => { const token = window.sessionStorage .getItem('jwt') ?.substring(1) .slice(0, -1); if (token) config.headers!.Authorization = `Bearer ${token}`; return config; }, (error) => Promise.reject(error) ); instance.interceptors.response.use( (response: AxiosResponse) => response, async (error: AxiosError) => { const originalRequest = error.config; if (error.message === 'Network Error' || error.response?.status === 401) { originalRequest._retry = true; // 问题:pca未定义,originalRequest.scopes不存在 const token = await pca.acquireTokenSilent({ scopes: originalRequest.scopes }); originalRequest.headers.Authorization = `Bearer ${token}`; return axios(originalRequest); } return Promise.reject(error); // 问题:此代码永远不会执行,因为前面已经return if (error.message === 'Network Error' || error.response?.status === 401) { console.log('This is my new error', error.response); } throw error.response; } ); const responseBody = (response: AxiosResponse) => response.data; const requests = { get: (url: string) => instance.get(url).then(responseBody), post: (url: string, body: {}) => instance.post(url, body).then(responseBody), put: (url: string, body: {}) => instance.put(url, body).then(responseBody), del: (url: string) => instance.delete(url).then(responseBody), }; const Incidents = { list: (): Promise<IIncident[]> => requests.get('/incidents'), new: (): Promise<string> => requests.get('/incidents/new'), details: (id: string): Promise<IIncident> => requests.get(`/incidents/${id}`), create: (incident: Partial<IIncident>) => requests.post('/incidents', incident), update: (incident: IIncident) => requests.put(`/incidents/${incident.id}`, incident), delete: (id: string) => requests.del(`/incidents/${id}`), }; const Categories = { list: () => requests.get('/categories'), details: (id: string) => requests.get(`/categories/${id}`), create: (category: ICategory) => requests.post('/categories', category), update: (category: ICategory) => requests.put(`/categories/${category.id}`, category), delete: (id: string) => requests.del(`/categories/${id}`), }; export { Incidents, Categories };
AuthContext.tsx
import React, { useContext, createContext, useState, MouseEventHandler, useEffect, } from 'react'; import { AuthCodeMSALBrowserAuthenticationProvider } from '@microsoft/microsoft-graph-client/authProviders/authCodeMsalBrowser'; import { User } from 'microsoft-graph'; import { InteractionRequiredAuthError, InteractionType, PublicClientApplication, } from '@azure/msal-browser'; import { useMsal } from '@azure/msal-react'; import { getAllAADUsers, getAvatar, getUser } from '../services/GraphService'; import { msalConfig } from '../utils/authConfig'; import useSessionStorage from '../../../hooks/useSessionStorage'; export interface AppApi { token?: string; } export interface AppUser { avatar?: string; displayName?: string; email?: string; id?: string; roles?: string | string[]; } export interface AppError { message: string; debug?: string; } type AppContext = { user?: AppUser; allADUsers?: User[]; error?: AppError; signIn?: MouseEventHandler<HTMLElement>; signOut?: MouseEventHandler<HTMLElement>; displayError?: Function; clearError?: Function; authProvider?: AuthCodeMSALBrowserAuthenticationProvider; apiToken?: AppApi; }; const appContext = createContext<AppContext>({ user: undefined, allADUsers: undefined, error: undefined, signIn: undefined, signOut: undefined, displayError: undefined, clearError: undefined, authProvider: undefined, apiToken: undefined, }); export function useAppContext(): AppContext { return useContext(appContext); } interface ProvideAppContextProps { children: React.ReactNode; } export default function ProvideAuthContext({ children, }: ProvideAppContextProps) { const auth = useProvideAppContext(); return <appContext.Provider value={auth}>{children}</appContext.Provider>; } function useProvideAppContext() { const msal = useMsal(); const [apiToken, setApiToken] = useState<AppApi | undefined>(undefined); const [user, setUser] = useState<AppUser | undefined>(undefined); const [error, setError] = useState<AppError | undefined>(undefined); const [allADUsers, setAllADUsers] = useState<User[] | undefined>(undefined); const [_, setValue] = useSessionStorage('jwt', ''); const displayError = (message: string, debug?: string) => { setError({ message, debug }); }; const clearError = () => { setError(undefined); }; // Used by the Graph SDK to authenticate API calls const authProvider = new AuthCodeMSALBrowserAuthenticationProvider( msal.instance as PublicClientApplication, { account: msal.instance.getActiveAccount()!, scopes: msalConfig.scopeGraph.scopes, interactionType: InteractionType.Popup, } ); useEffect(() => { const checkUser = async () => { if (!user) { try { // Check if user is already signed in const account = msal.instance.getActiveAccount(); if (account) { // Get the user from Microsoft Graph const user = await getUser(authProvider); const avatar = await getAvatar(authProvider); setUser({ avatar: avatar || '', displayName: user.displayName || '', email: user.mail || user.userPrincipalName || '', id: user.id || '', roles: account.idTokenClaims?.roles || '', }); setAllADUsers(await getAllAADUsers(authProvider)); ///Getting the Api Token if one is needed await getApiToken(); } } catch (err: any) { displayError(err.message); } } }; checkUser(); }); const signIn = async () => { const accounts = await msal.instance.loginPopup({ scopes: msalConfig.scopeGraph.scopes, prompt: 'select_account', }); console.log('signIn'); //Getting the Api Token if one is needed await getApiToken(); // Get the user from Microsoft Graph const user = await getUser(authProvider); const avatar = await getAvatar(authProvider); setAllADUsers(await getAllAADUsers(authProvider)); setUser({ avatar: avatar || '', displayName: user.displayName || '', email: user.mail || user.userPrincipalName || '', id: user.id || '', roles: accounts.account?.idTokenClaims?.roles || '', }); }; const signOut = async () => { await msal.instance.logoutPopup(); setUser(undefined); setApiToken(undefined); window.sessionStorage.removeItem('jwt'); }; const getApiToken = async () => { if (msalConfig.scopeAPI.scopes.length > 0) { msal.instance .acquireTokenSilent({ scopes: msalConfig.scopeAPI.scopes, }) .then((response) => { setValue(response.accessToken); setApiToken({ token: response.accessToken, }); }) .catch((error) => { if (error instanceof InteractionRequiredAuthError) { msal.instance .acquireTokenPopup({ scopes: msalConfig.scopeAPI.scopes, prompt: 'select_account', }) .then(function (response) { setApiToken({ token: response.accessToken, }); }) .catch(function (error) { console.log(error); }); } }); } }; return { user, allADUsers, error, signIn, signOut, displayError, clearError, authProvider, apiToken, }; }
解决方案
问题1:处理Network Error与401状态异常
原因分析
- Network Error:通常是CORS配置错误、浏览器拦截请求、服务端未启动或网络中断导致,此时Axios无法获取服务端返回的状态码。
- 原代码存在逻辑错误:重复的错误判断、
pca未定义、originalRequest.scopes不存在,且return Promise.reject(error)后代码无法执行。
修复步骤
- 排查服务端CORS配置,确保允许前端域名的跨域请求;
- 修正Axios拦截器逻辑,区分网络错误和401令牌过期错误;
- 添加请求重试标记,避免无限循环。
问题2:在agent.ts中调用MSAL获取令牌(不使用Hooks)
核心思路
agent.ts作为工具类无法使用React Hooks,可直接导入MSAL的PublicClientApplication实例,调用令牌刷新方法。
具体实现
- 导出MSAL实例:在
authConfig.ts中创建并导出MSAL实例
// src/utils/authConfig.ts import { PublicClientApplication } from '@azure/msal-browser'; export const msalConfig = { auth: { clientId: '你的客户端ID', authority: '你的授权地址', redirectUri: '你的重定向地址', }, scopeGraph: { scopes: ['user.read'] }, scopeAPI: { scopes: ['api://你的API范围/access'] }, }; export const pca = new PublicClientApplication(msalConfig);
- 修改agent.ts的响应拦截器
// agent.ts import { pca, msalConfig } from '../utils/authConfig'; // ... 其他代码保持不变 instance.interceptors.response.use( (response: AxiosResponse) => response, async (error: AxiosError) => { const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean }; // 避免重复重试,防止无限循环 if (originalRequest._retry) { return Promise.reject(error); } // 处理401令牌过期 if (error.response?.status === 401) { originalRequest._retry = true; try { const activeAccount = pca.getActiveAccount(); if (!activeAccount) { // 无活跃账户,跳转登录 window.location.href = '/login'; return Promise.reject(error); } // 静默刷新令牌 const tokenResponse = await pca.acquireTokenSilent({ scopes: msalConfig.scopeAPI.scopes, account: activeAccount, }); // 更新sessionStorage和请求头 window.sessionStorage.setItem('jwt', JSON.stringify(tokenResponse.accessToken)); originalRequest.headers!.Authorization = `Bearer ${tokenResponse.accessToken}`; // 重新发起请求 return instance(originalRequest); } catch (tokenError) { // 静默刷新失败,触发弹窗登录 if (tokenError instanceof InteractionRequiredAuthError) { try { const tokenResponse = await pca.acquireTokenPopup({ scopes: msalConfig.scopeAPI.scopes, }); window.sessionStorage.setItem('jwt', JSON.stringify(tokenResponse.accessToken)); originalRequest.headers!.Authorization = `Bearer ${tokenResponse.accessToken}`; return instance(originalRequest); } catch (popupError) { return Promise.reject(popupError); } } return Promise.reject(tokenError); } } // 处理Network Error if (error.message === 'Network Error') { console.error('网络连接异常,请检查网络或服务状态'); } return Promise.reject(error); } );
- 修正请求拦截器的令牌读取逻辑
原代码中substring(1).slice(0, -1)是因为存储时用了JSON.stringify,可直接解析:
instance.interceptors.request.use( (config: AxiosRequestConfig) => { const tokenStr = window.sessionStorage.getItem('jwt'); const token = tokenStr ? JSON.parse(tokenStr) : null; if (token) config.headers!.Authorization = `Bearer ${token}`; return config; }, (error) => Promise.reject(error) );
额外优化建议
- 在AuthContext中监听令牌变化,自动更新sessionStorage;
- 添加全局错误处理,统一提示用户令牌过期或网络错误;
- 为Axios请求添加超时时间,避免无限等待。
内容的提问来源于stack exchange,提问作者Jean-François Handfield
相关产品推荐
相关产品推荐

