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

基于Axios拦截器从MSAL获取刷新令牌的问题排查

Axios 401错误时从MSAL获取新令牌的问题解决

问题概述

在React项目中使用Axios和MSAL时,遇到两个核心问题:

  1. Axios请求失败时,错误状态为0或undefined,仅返回“Network Error”,无法准确识别401过期令牌错误;
  2. 封装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)后代码无法执行。

修复步骤

  1. 排查服务端CORS配置,确保允许前端域名的跨域请求;
  2. 修正Axios拦截器逻辑,区分网络错误和401令牌过期错误;
  3. 添加请求重试标记,避免无限循环。

问题2:在agent.ts中调用MSAL获取令牌(不使用Hooks)

核心思路

agent.ts作为工具类无法使用React Hooks,可直接导入MSAL的PublicClientApplication实例,调用令牌刷新方法。

具体实现

  1. 导出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);
  1. 修改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);
  }
);
  1. 修正请求拦截器的令牌读取逻辑
    原代码中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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 01:55:18