# OAuth 回调重复调用问题修复 ## 问题描述 在开发环境中,OAuth 回调端点 `/api/oauth/callback` 被调用了两次,导致第二次调用时出现错误: ```json { "detail": "OAuth 登录失败: 无效的令牌响应格式: {'error': 'invalid_grant', 'error_description': '授权码已被使用'}" } ``` ## 根本原因 ### React StrictMode 在开发环境中,React 18 的 StrictMode 会**故意**渲染组件两次,以帮助发现副作用问题。这导致 `useEffect` 被执行两次。 ```tsx // main.tsx ``` ### 错误的修复尝试 最初尝试使用 `useState` 来防止重复调用: ```tsx const [isProcessing, setIsProcessing] = useState(false); useEffect(() => { if (isProcessing) { return; // 试图阻止重复调用 } setIsProcessing(true); // ... 处理逻辑 }, [searchParams, navigate, setAuth, isProcessing]); // ❌ 问题在这里! ``` **问题**:`isProcessing` 被添加到依赖数组中,当它从 `false` 变为 `true` 时,会触发 `useEffect` 重新运行,从而绕过了防重复调用的检查。 ## 正确的解决方案 使用 `useRef` 而不是 `useState` 来跟踪处理状态: ```tsx const isProcessingRef = useRef(false); useEffect(() => { // 防止 React StrictMode 导致的重复调用 if (isProcessingRef.current) { console.log('Already processing, skipping duplicate call'); return; } const processCallback = async () => { isProcessingRef.current = true; console.log('Starting OAuth callback processing...'); try { // ... OAuth 处理逻辑 } catch (err) { // ... 错误处理 } }; processCallback(); }, [searchParams, navigate, setAuth]); // ✅ 不包含 isProcessingRef ``` ### 为什么 useRef 有效? 1. **不触发重渲染**:修改 `ref.current` 不会触发组件重渲染 2. **不在依赖数组中**:`useRef` 返回的对象在组件生命周期内保持不变,不需要添加到依赖数组 3. **跨渲染持久化**:即使组件被渲染两次,`ref` 的值也会保持 ## 执行流程 ### StrictMode 下的执行顺序 ``` 1. 第一次渲染 - isProcessingRef.current = false - 检查通过,开始处理 - isProcessingRef.current = true - 调用 /api/oauth/callback (第一次) 2. StrictMode 触发第二次渲染 - isProcessingRef.current = true (保持不变) - 检查失败,跳过处理 - 不调用 /api/oauth/callback 结果:只调用一次 ✅ ``` ### 使用 useState 的错误流程 ``` 1. 第一次渲染 - isProcessing = false - 检查通过,开始处理 - setIsProcessing(true) - 调用 /api/oauth/callback (第一次) 2. isProcessing 变化触发 useEffect 重新运行 - isProcessing = true - 检查失败,跳过... 等等! 3. StrictMode 触发第二次渲染 - isProcessing = false (重置) - 检查通过,开始处理 - 调用 /api/oauth/callback (第二次) ❌ 结果:调用两次 ❌ ``` ## 测试验证 ### 开发环境测试 1. 启动前端:`cd web && yarn nx serve lq_label` 2. 访问:`http://localhost:4200/login` 3. 点击"使用 SSO 登录" 4. 完成 OAuth 登录 5. 查看浏览器控制台: ``` OAuth callback URL parameters: {code: "...", state: "..."} Starting OAuth callback processing... Code: NhbmBfgF4XMJcsAlykbs2GOAec_0ylqu9JcAKvCcffA State: O_Fo8TTBRAHdTxw4GTZD_ZmvAz0fD1KnQIo4TE337fc Exchanging code for tokens... Already processing, skipping duplicate call ← 第二次调用被阻止 Token exchange successful ``` 6. 查看后端日志: ``` INFO: 127.0.0.1:xxxxx - "GET /api/oauth/callback?code=...&state=... HTTP/1.1" 200 OK ``` **只有一次请求!** ✅ ### 生产环境 在生产环境中,StrictMode 会被禁用,所以不会有重复调用问题。但保留这个修复不会有任何负面影响。 ## 其他解决方案(不推荐) ### 方案 1:禁用 StrictMode(不推荐) ```tsx // main.tsx // ❌ 不推荐:失去 StrictMode 的开发时检查 ``` **缺点**:失去 React 18 的开发时检查和警告。 ### 方案 2:使用 AbortController(过度设计) ```tsx useEffect(() => { const controller = new AbortController(); const processCallback = async () => { try { const response = await fetch('/api/oauth/callback', { signal: controller.signal }); // ... } catch (err) { if (err.name === 'AbortError') return; // ... } }; processCallback(); return () => controller.abort(); }, [searchParams]); ``` **缺点**:更复杂,且第一次请求仍然会被发送(只是被取消)。 ## 总结 - ✅ **使用 `useRef`** 跟踪处理状态 - ✅ **不要将 ref 添加到依赖数组** - ✅ **保留 StrictMode** 以获得开发时检查 - ✅ **添加日志** 以便调试 这个修复简单、有效,且不影响生产环境的行为。 --- **修复日期**: 2024-01-22 **文件**: `web/apps/lq_label/src/components/oauth-callback/oauth-callback.tsx` **状态**: ✅ 已修复