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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 06:55:57