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

如何基于React Azure MSAL SSO在令牌过期时重定向至登录页?

解决ReactJS MSAL令牌过期后无法重定向至登录页的问题

问题定位

现有代码的核心问题在于:

  • 仅处理了令牌即将过期(剩余时间≤10分钟)的刷新逻辑,未覆盖令牌已完全过期的场景
  • 当静默刷新和交互刷新都失败时,没有触发强制登录重定向的逻辑
  • 依赖sessionStorage存储令牌,容易和MSAL内部状态不同步

修正方案

以下是调整后的代码,重点优化令牌过期检测、刷新失败后的处理逻辑,并利用MSAL内置事件增强可靠性:

修改后的MainContent.js

import React, { useEffect } from "react";
import {
  AuthenticatedTemplate,
  useMsal,
  UnauthenticatedTemplate,
} from "@azure/msal-react";
import { useShallow } from "zustand/react/shallow";
import useAuthStore from "../store/authStore";
import { jwtDecode } from "jwt-decode";
import AppRoutes from "../routes/AppRoutes";
import AutoSignIn from "./AutoSignIn";
import { InteractionRequiredAuthError } from "@azure/msal-browser";

const MainContent = () => {
  const { instance, accounts } = useMsal();
  const activeAccount = instance.getActiveAccount();
  const REFRESH_THRESHOLD = 600; // 10分钟阈值

  const { setLoginState, isLoggedOut } = useAuthStore(
    useShallow((state) => ({
      setLoginState: state.setLoginState,
      isLoggedOut: state.isLoggedOut,
    }))
  );

  // 保存令牌到sessionStorage(同步MSAL状态)
  const saveActiveToken = () => {
    if (activeAccount?.idToken) {
      sessionStorage.setItem("access-token", activeAccount.idToken);
    }
  };

  // 登出并跳转登录页
  const handleForceLoginRedirect = async () => {
    // 清除本地账户信息
    instance.removeAccount(activeAccount);
    sessionStorage.removeItem("access-token");
    // 触发登录重定向
    await instance.loginRedirect().catch(err => console.error("登录重定向失败:", err));
  };

  // 令牌刷新逻辑
  const refreshToken = async () => {
    try {
      const response = await instance.acquireTokenSilent({
        account: activeAccount,
        scopes: ["openid", "profile", "email"] // 替换为你的实际权限范围
      });
      // 刷新成功后更新令牌
      saveActiveToken();
    } catch (error) {
      if (error instanceof InteractionRequiredAuthError) {
        try {
          // 尝试交互刷新
          await instance.acquireTokenRedirect({
            account: activeAccount,
            scopes: ["openid", "profile", "email"]
          });
        } catch (redirectErr) {
          // 交互刷新失败,强制跳转登录
          handleForceLoginRedirect();
        }
      } else {
        // 其他错误,直接触发登录
        handleForceLoginRedirect();
      }
    }
  };

  // 令牌过期检查定时器
  useEffect(() => {
    const checkTokenExpiry = setInterval(() => {
      try {
        const token = sessionStorage.getItem("access-token");
        if (!token) return;

        const decodedToken = jwtDecode(token);
        const currentTime = Math.floor(Date.now() / 1000);
        const timeUntilExpiry = decodedToken.exp - currentTime;

        if (timeUntilExpiry <= 0) {
          // 令牌已过期,强制跳转登录
          handleForceLoginRedirect();
        } else if (timeUntilExpiry <= REFRESH_THRESHOLD) {
          // 令牌即将过期,触发刷新
          refreshToken();
        }
      } catch (err) {
        console.error("令牌检查错误:", err);
        // 解析失败时强制登录
        handleForceLoginRedirect();
      }
    }, 60000); // 每分钟检查一次

    return () => clearInterval(checkTokenExpiry);
  }, [activeAccount]);

  // 监听MSAL内置事件(增强可靠性)
  useEffect(() => {
    // 监听令牌过期事件
    const tokenExpiredHandler = () => {
      handleForceLoginRedirect();
    };

    // 监听账户移除事件
    const accountRemovedHandler = () => {
      sessionStorage.removeItem("access-token");
    };

    instance.addEventCallback((event) => {
      if (event.eventType === "tokenExpired") {
        tokenExpiredHandler();
      } else if (event.eventType === "accountRemoved") {
        accountRemovedHandler();
      }
    });

    return () => {
      instance.removeEventCallback(tokenExpiredHandler);
      instance.removeEventCallback(accountRemovedHandler);
    };
  }, [instance]);

  // 处理主动登出
  useEffect(() => {
    if (isLoggedOut) {
      instance.logoutRedirect().catch(err => console.error("登出失败:", err));
    }
  }, [isLoggedOut, instance]);

  // 组件挂载时同步令牌
  useEffect(() => {
    saveActiveToken();
  }, [activeAccount]);

  return (
    <div>
      <AuthenticatedTemplate>
        {activeAccount ? <AppRoutes /> : null}
      </AuthenticatedTemplate>
      <UnauthenticatedTemplate>
        <AutoSignIn instance={instance} />
      </UnauthenticatedTemplate>
    </div>
  );
};

export default MainContent;

关键修改说明

  • 新增令牌已过期处理:在定时检查中判断timeUntilExpiry <= 0,直接触发登录重定向
  • 完善刷新失败逻辑:当静默刷新和交互刷新都失败时,清除本地账户信息并强制跳转登录
  • 添加MSAL事件监听:利用MSAL的tokenExpired和accountRemoved事件,增强过期处理的可靠性
  • 同步令牌状态:组件挂载和账户变化时同步令牌到sessionStorage,避免状态不一致
  • 优化权限范围:在acquireTokenSilent和acquireTokenRedirect中明确指定权限范围,确保刷新请求有效

内容的提问来源于stack exchange,提问作者F-Developer

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 18:35:07