# 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`
**状态**: ✅ 已修复