Azure AD SSO组件useEffect多次执行致授权码重复使用报错
React集成Azure AD SSO时授权码重复兑换导致AADSTS54005错误的解决方案
问题背景
在React应用中集成Azure AD SSO时,触发AADSTS54005: OAuth2 Authorization code was already redeemed, please retry with a new valid code or use an existing refresh token错误,原因是应用多次重复使用URL中的授权码。排查AzureRedirect.jsx的useEffect逻辑(包括添加日志、调整逻辑位置、修改依赖项等)后仍未彻底解决,关闭React严格模式可暂时缓解,但需要长期可靠的方案确保dispatch(azureRedirect(code))仅执行一次。
可行解决方案
1. 用useRef标记授权码处理状态
通过useRef创建一个持久化标记,确保授权码仅被处理一次,不会因组件重渲染或严格模式的二次执行重复触发:
import { useEffect, useRef } from 'react'; import { useDispatch } from 'react-redux'; const AzureRedirect = () => { const dispatch = useDispatch(); const hasProcessedCode = useRef(false); useEffect(() => { if (hasProcessedCode.current) return; const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get('code'); if (code) { hasProcessedCode.current = true; dispatch(azureRedirect(code)) .then(() => { // 清除URL中的code参数,避免刷新页面再次触发 window.history.replaceState({}, document.title, window.location.pathname); // 跳转到目标页面 window.location.href = '/dashboard'; }) .catch(() => { // 处理错误时重置标记,允许重新尝试 hasProcessedCode.current = false; }); } }, [dispatch]); return <div>登录处理中...</div>; };
useRef的状态变更不会触发组件重渲染,适合这种一次性执行的场景。
2. 处理完成后清除URL中的授权码
在授权码兑换成功后,使用window.history.replaceState移除URL中的code参数,从根源上避免后续逻辑读取到已使用的授权码:
dispatch(azureRedirect(code)).then(() => { window.history.replaceState({}, document.title, window.location.pathname); });
3. 给异步请求添加幂等性校验
在authService的授权码兑换方法中,添加全局锁避免重复请求:
// authService.js let isProcessing = false; export const azureRedirect = async (code) => { if (isProcessing) { return Promise.reject(new Error('当前已有请求在处理中')); } isProcessing = true; try { // 调用后端接口兑换授权码 const res = await fetch('/api/auth/azure-redirect', { method: 'POST', body: JSON.stringify({ code }), headers: { 'Content-Type': 'application/json' } }); return await res.json(); } finally { isProcessing = false; } };
4. 优化Redux依赖项与登录状态判断
结合Redux的用户状态,跳过已登录场景的授权码处理:
import { useSelector } from 'react-redux'; const AzureRedirect = () => { const dispatch = useDispatch(); const currentUser = useSelector(state => state.auth.user); const hasProcessedCode = useRef(false); useEffect(() => { // 已有用户登录或已处理过授权码,直接跳过 if (currentUser || hasProcessedCode.current) return; const code = new URLSearchParams(window.location.search).get('code'); if (code) { hasProcessedCode.current = true; dispatch(azureRedirect(code)).then(() => { window.history.replaceState({}, document.title, window.location.pathname); window.location.href = '/dashboard'; }); } }, [dispatch, currentUser]); };
确保useSelector返回的用户状态引用稳定,避免不必要的useEffect触发。
5. 后端层面增加授权码重复使用防护
在后端userController中,用Redis记录已使用的授权码(设置与授权码有效期匹配的过期时间),拦截重复请求:
// userController.js const redis = require('redis'); const client = redis.createClient(); exports.azureRedirect = async (req, res) => { const { code } = req.body; const usedCodeKey = `auth_code_used:${code}`; const isUsed = await client.get(usedCodeKey); if (isUsed) { return res.status(400).json({ error: '授权码已被使用' }); } try { // 调用Azure AD接口兑换令牌 const tokenRes = await azureAdExchangeCode(code); // 标记授权码已使用,过期时间设为5分钟(Azure AD授权码默认有效期) await client.setEx(usedCodeKey, 300, '1'); // 处理用户登录逻辑 res.json({ user: tokenRes.user }); } catch (err) { res.status(500).json({ error: err.message }); } };
内容的提问来源于stack exchange,提问作者Shea Erickson
相关产品推荐
相关产品推荐

