非React组件下Axios拦截器集成MSAL的交互问题解决方案咨询
问题
我使用Axios发送请求,通过MSAL实现身份认证。为避免在每个请求中手动附加Access Token,采用Axios拦截器进行全局处理。由于该代码不属于React组件,无法使用MSAL Hooks,因此我导出了传入MsalProvider的PublicClientApplication实例。
我不确定此方案是否为最优解,且当前实现无法处理需要交互的场景(已注释掉相关重定向代码)。根据微软官方文档,在React组件外调用acquireTokenSilent失败时,不应回退到交互操作,所有交互需在MsalProvider组件树内完成。若移除注释代码则缺失交互处理能力,请问有哪些可行的替代实现方式?
当前代码如下:
import { InteractionRequiredAuthError } from '@azure/msal-browser' import axios from 'axios' import { azureAdInstance } from '../index' const axiosInstance = axios.create({ baseURL: "https://myapi.com", }) axiosInstance.interceptors.request.use(async (request) => { const instance = azureAdInstance const accounts = azureAdInstance.getAllAccounts() const accessTokenRequest = { scopes: ["scope1"], account: accounts[0], } // 静默获取Access Token并附加到请求中 try { const { accessToken } = await instance.acquireTokenSilent( accessTokenRequest ) if (accessToken) request.headers.Authorization = `Bearer ${accessToken}` } catch (error) { // 根据微软文档不应使用 // if (error instanceof InteractionRequiredAuthError) { // instance.acquireTokenRedirect(accessTokenRequest) // } console.log(error) } return request }) export default axiosInstance
可行的替代实现方式
方案1:将交互逻辑委托给React组件内的MSAL回调
核心思路是在拦截器捕获InteractionRequiredAuthError时,通过事件触发或状态通知,让MsalProvider树内的组件处理交互请求,完成后再重试原请求。
实现步骤
- 搭建一个简单的全局事件总线,用于传递认证交互需求和请求缓存信息。
- 在Axios拦截器中捕获交互错误时,触发事件并缓存当前请求,返回一个pending状态的Promise等待后续处理。
- 在MsalProvider包裹的组件中监听事件,调用MSAL的交互方法(如
acquireTokenPopup)完成认证。 - 交互成功获取token后,重新发起缓存的请求。
代码示例
1. 全局事件总线
// authEventBus.js const eventBus = { on(event, callback) { document.addEventListener(event, (e) => callback(e.detail)); }, emit(event, data) { document.dispatchEvent(new CustomEvent(event, { detail: data })); }, }; export default eventBus;
2. 修改Axios拦截器
import { InteractionRequiredAuthError } from '@azure/msal-browser' import axios from 'axios' import { azureAdInstance } from '../index' import eventBus from './authEventBus' const axiosInstance = axios.create({ baseURL: "https://myapi.com", }) // 缓存需要重试的请求 let pendingRequests = []; axiosInstance.interceptors.request.use(async (request) => { const instance = azureAdInstance const accounts = azureAdInstance.getAllAccounts() // 无登录账号时触发登录交互 if (!accounts.length) { eventBus.emit('auth:require-login', { request }); return new Promise((resolve) => { pendingRequests.push({ resolve, request }); }); } const accessTokenRequest = { scopes: ["scope1"], account: accounts[0], } try { const { accessToken } = await instance.acquireTokenSilent(accessTokenRequest) if (accessToken) request.headers.Authorization = `Bearer ${accessToken}` return request } catch (error) { if (error instanceof InteractionRequiredAuthError) { // 触发交互获取token的事件 eventBus.emit('auth:require-interaction', { request, tokenRequest: accessTokenRequest }); return new Promise((resolve) => { pendingRequests.push({ resolve, request }); }); } console.log(error) return Promise.reject(error) } }) // 供组件调用的重试方法 export const retryPendingRequests = (accessToken) => { pendingRequests.forEach(({ resolve, request }) => { if (accessToken) { request.headers.Authorization = `Bearer ${accessToken}`; } resolve(request); }); pendingRequests = []; }; export default axiosInstance
3. 在MsalProvider内添加交互处理组件
// AuthHandler.jsx import { useMsal } from '@azure/msal-react' import { useEffect } from 'react' import eventBus from './authEventBus' import { retryPendingRequests } from './axiosInstance' const AuthHandler = () => { const { instance, accounts } = useMsal() useEffect(() => { // 处理未登录场景 const handleLoginRequired = async ({ request }) => { if (!accounts.length) { await instance.loginPopup({ scopes: ["scope1"] }); const { accessToken } = await instance.acquireTokenSilent({ scopes: ["scope1"], account: instance.getAllAccounts()[0] }); retryPendingRequests(accessToken); } } // 处理需要交互获取token的场景 const handleInteractionRequired = async ({ tokenRequest }) => { try { const result = await instance.acquireTokenPopup(tokenRequest); retryPendingRequests(result.accessToken); } catch (err) { console.error('交互认证失败:', err); pendingRequests = []; } } eventBus.on('auth:require-login', handleLoginRequired); eventBus.on('auth:require-interaction', handleInteractionRequired); return () => { document.removeEventListener('auth:require-login', handleLoginRequired); document.removeEventListener('auth:require-interaction', handleInteractionRequired); } }, [instance, accounts]) return null; } export default AuthHandler
4. 在App中引入该组件(需在MsalProvider内部)
// App.jsx import { MsalProvider } from '@azure/msal-react' import { azureAdInstance } from './index' import AuthHandler from './AuthHandler' import YourRoutes from './Routes' function App() { return ( <MsalProvider instance={azureAdInstance}> <AuthHandler /> <YourRoutes /> </MsalProvider> ) } export default App
方案2:封装Axios实例为React Hook
如果请求大多在React组件内发起,可以将Axios实例封装成Hook,利用MSAL Hooks在组件内直接处理token获取和交互逻辑,避免在非组件代码中处理认证。
代码示例
// useAxios.js import { useMsal } from '@azure/msal-react' import axios from 'axios' import { useEffect, useMemo } from 'react' const useAxios = () => { const { instance, accounts } = useMsal() const axiosInstance = useMemo(() => axios.create({ baseURL: "https://myapi.com" }), []) useEffect(() => { const requestInterceptor = axiosInstance.interceptors.request.use(async (request) => { if (!accounts.length) return Promise.reject(new Error('未登录')) const accessTokenRequest = { scopes: ["scope1"], account: accounts[0] } try { const { accessToken } = await instance.acquireTokenSilent(accessTokenRequest) request.headers.Authorization = `Bearer ${accessToken}` return request } catch (error) { if (error instanceof InteractionRequiredAuthError) { // 直接在组件内调用交互方法 const result = await instance.acquireTokenPopup(accessTokenRequest) request.headers.Authorization = `Bearer ${result.accessToken}` return request } return Promise.reject(error) } }) return () => { axiosInstance.interceptors.request.eject(requestInterceptor) } }, [instance, accounts, axiosInstance]) return axiosInstance } export default useAxios
组件内使用
// MyComponent.jsx import useAxios from './useAxios' const MyComponent = () => { const axiosInstance = useAxios() const fetchData = async () => { try { const response = await axiosInstance.get('/data') console.log(response.data) } catch (err) { console.error(err) } } return <button onClick={fetchData}>获取数据</button> } export default MyComponent
方案3:利用MSAL内置拦截器
MSAL浏览器库提供了addInterceptor方法,可以自动为请求附加token,交互逻辑可委托给组件内处理(适合以fetch发起请求的场景,Axios需额外适配)。
代码示例(MSAL初始化处)
// index.js import { PublicClientApplication, InteractionRequiredAuthError } from '@azure/msal-browser' import { MsalProvider } from '@azure/msal-react' import eventBus from './authEventBus' import { retryPendingRequests } from './axiosInstance' export const azureAdInstance = new PublicClientApplication({ auth: { clientId: 'your-client-id', authority: 'your-authority', redirectUri: 'your-redirect-uri' } }) // 添加MSAL请求拦截器 azureAdInstance.addInterceptor({ async beforeRequest(request) { const accounts = azureAdInstance.getAllAccounts() if (!accounts.length) { eventBus.emit('auth:require-login', {}); return; } try { const tokenResult = await azureAdInstance.acquireTokenSilent({ scopes: ["scope1"], account: accounts[0] }) request.headers.set('Authorization', `Bearer ${tokenResult.accessToken}`) } catch (error) { if (error instanceof InteractionRequiredAuthError) { eventBus.emit('auth:require-interaction', { tokenRequest: { scopes: ["scope1"], account: accounts[0] } }); } else { throw error } } } })
后续交互逻辑处理同方案1,在MsalProvider内的组件中监听事件完成认证并重试请求。
内容的提问来源于stack exchange,提问作者Tom el Safadi
相关产品推荐
相关产品推荐

