如何基于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
相关产品推荐
相关产品推荐

