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

Azure MSAL React中如何正确处理令牌过期与刷新?

处理Azure MSAL React中令牌过期与自动刷新的问题

问题描述

在React应用中使用Azure SSO登录后,将令牌存入localStorage,但令牌即将过期或已过期时,无法通过自定义Hook正确获取新令牌并更新localStorage,导致API请求返回401错误。当前实现了每3秒轮询的Hook检查令牌过期,调用acquireTokenSilent刷新令牌,但逻辑未生效。

现有代码

import { useEffect, useRef } from 'react';
import { useMsal } from '@azure/msal-react';
import jwt from 'jwt-decode';
import { backendScopeRequest } from '../config/authConfig';
import { useAuthStore } from '../store/auth';

const REFRESH_THRESHOLD = 300; // 5 minutes in seconds

export const useBackendTokenCheckExpirationTime = () => {
  const interval = useRef(null);
  const { instance, accounts } = useMsal();
  const { updateBackendAccessToken, updateLoggedInUserProfile } = useAuthStore();

  const setRefreshTime = () => {
    const backendAccessToken = localStorage.getItem('backendAccessToken');
    if (backendAccessToken) {
      const decodeToken = jwt(backendAccessToken);
      const currentTime = Math.floor(Date.now() / 1000); // Current time in seconds
      const timeUntilExpiry = decodeToken.exp - currentTime - REFRESH_THRESHOLD;
      localStorage.setItem('backendRefreshTime', timeUntilExpiry);
      return timeUntilExpiry;
    }
    return null;
  };

  const handleLogout = () => {};

  const acquireTokenWithRefreshToken = async () => {
    try {
      if (accounts.length && instance) {
        const response = await instance.acquireTokenSilent({
          account: accounts[0],
          ...backendScopeRequest,
        });
        const decodeToken = jwt(response.accessToken);
        localStorage.setItem('backendAccessToken', response.accessToken);
        updateBackendAccessToken(response.accessToken);
        updateLoggedInUserProfile(decodeToken);
        const currentTime = Math.floor(Date.now() / 1000);
        const timeUntilExpiry = decodeToken.exp - currentTime - REFRESH_THRESHOLD;
        localStorage.setItem('backendRefreshTime', timeUntilExpiry);
      }
    } catch (error) {
      console.log('error', error);
      handleLogout();
      // Handle token refresh error
    }
  };

  useEffect(() => {
    interval.current = setInterval(() => {
      const backendRefreshTime = setRefreshTime();
      if (backendRefreshTime !== null && backendRefreshTime <= 0) {
        acquireTokenWithRefreshToken();
      }
    }, 3000);

    return () => clearInterval(interval.current);
  }, []);
};

代码问题分析

  1. useEffect依赖缺失:useEffect的依赖数组为空,导致instance和accounts始终是组件挂载时的初始值。用户登录后accounts更新,但Hook不会重新执行,导致acquireTokenSilent无法拿到有效账户。
  2. 手动维护令牌缓存:MSAL库本身会自动管理令牌缓存,手动将令牌存入localStorage容易导致缓存不一致,MSAL返回的新令牌可能未及时同步到localStorage。
  3. 固定轮询间隔不合理:每3秒轮询过于频繁,且未根据令牌实际过期时间动态调整检查时机,浪费资源。
  4. 错误处理不完善:acquireTokenSilent在令牌已过期或刷新令牌无效时会抛出错误,仅调用handleLogout不够,应提供交互式登录的回退方案。

正确的令牌刷新实现方案

步骤1:修复useEffect依赖

将instance和accounts加入useEffect依赖数组,确保账户更新时Hook能重新初始化检查逻辑。

步骤2:利用MSAL内置缓存

无需手动维护localStorage,直接从MSAL实例获取令牌和账户信息,避免缓存不一致。

步骤3:动态设置刷新定时器

根据令牌过期时间计算下次刷新的延迟,替换固定轮询,减少不必要的检查。

步骤4:完善错误处理

当acquireTokenSilent失败时,调用acquireTokenPopup或acquireTokenRedirect触发交互式登录,避免直接登出用户。

优化后的代码

import { useEffect, useRef } from 'react';
import { useMsal } from '@azure/msal-react';
import jwt from 'jwt-decode';
import { backendScopeRequest } from '../config/authConfig';
import { useAuthStore } from '../store/auth';

const REFRESH_THRESHOLD = 300; // 提前5分钟刷新

export const useTokenRefresh = () => {
  const timerRef = useRef(null);
  const { instance, accounts } = useMsal();
  const { updateBackendAccessToken, updateLoggedInUserProfile } = useAuthStore();

  // 获取当前有效账户
  const getActiveAccount = () => {
    return accounts.length ? accounts[0] : instance.getActiveAccount();
  };

  // 计算令牌剩余有效时间
  const calculateTimeToRefresh = (token) => {
    const decoded = jwt(token);
    const currentTime = Math.floor(Date.now() / 1000);
    // 减去阈值,提前触发刷新
    return (decoded.exp - currentTime - REFRESH_THRESHOLD) * 1000;
  };

  // 刷新令牌
  const refreshToken = async () => {
    const account = getActiveAccount();
    if (!account || !instance) return;

    try {
      const response = await instance.acquireTokenSilent({
        account,
        ...backendScopeRequest,
      });

      // 更新状态管理,无需存localStorage
      updateBackendAccessToken(response.accessToken);
      updateLoggedInUserProfile(jwt(response.accessToken));

      // 设置下一次刷新定时器
      scheduleNextRefresh(response.accessToken);
    } catch (error) {
      console.error('静默刷新令牌失败:', error);
      // 静默失败时,触发交互式登录
      if (error.name === 'InteractionRequiredAuthError') {
        try {
          await instance.acquireTokenPopup({
            ...backendScopeRequest,
            account,
          });
        } catch (interactiveError) {
          console.error('交互式登录失败:', interactiveError);
          // 最终失败再执行登出
          instance.logoutRedirect();
        }
      } else {
        instance.logoutRedirect();
      }
    }
  };

  // 安排下一次刷新
  const scheduleNextRefresh = (token) => {
    if (timerRef.current) clearTimeout(timerRef.current);
    const timeToRefresh = calculateTimeToRefresh(token);
    
    // 如果剩余时间小于0,立即刷新
    if (timeToRefresh <= 0) {
      refreshToken();
      return;
    }

    timerRef.current = setTimeout(refreshToken, timeToRefresh);
  };

  useEffect(() => {
    const account = getActiveAccount();
    if (!account) return;

    // 初始化时检查当前令牌状态
    instance.getTokenCache().getAccessTokenByScopes(backendScopeRequest.scopes, account)
      .then((token) => {
        if (token) {
          scheduleNextRefresh(token.secret);
        } else {
          // 无有效令牌,立即刷新
          refreshToken();
        }
      });

    // 清理定时器
    return () => {
      if (timerRef.current) clearTimeout(timerRef.current);
    };
  }, [instance, accounts]);
};

关键注意事项

  • 优先使用MSAL缓存:MSAL会自动处理令牌的缓存、刷新和过期,手动存储令牌到localStorage会破坏MSAL的内置逻辑,导致状态不一致。
  • 交互式登录回退:当静默刷新失败(比如用户长时间未操作,刷新令牌过期),必须触发交互式登录,否则无法获取新令牌。
  • 动态定时器:根据令牌过期时间设置刷新延迟,避免不必要的轮询,提升性能。

内容的提问来源于stack exchange,提问作者SDK

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.10 20:25:59