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); }, []); };
代码问题分析
- useEffect依赖缺失:
useEffect的依赖数组为空,导致instance和accounts始终是组件挂载时的初始值。用户登录后accounts更新,但Hook不会重新执行,导致acquireTokenSilent无法拿到有效账户。 - 手动维护令牌缓存:MSAL库本身会自动管理令牌缓存,手动将令牌存入
localStorage容易导致缓存不一致,MSAL返回的新令牌可能未及时同步到localStorage。 - 固定轮询间隔不合理:每3秒轮询过于频繁,且未根据令牌实际过期时间动态调整检查时机,浪费资源。
- 错误处理不完善:
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
相关产品推荐
相关产品推荐

