video-flow-b/docs/google-oauth-callback-flow-analysis.md

558 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🔍 Google OAuth 回调逻辑全面梳理
## 📋 概述
Video Flow 项目的 Google OAuth 登录采用了**授权码模式**整个流程涉及前端、Java 认证服务和 Python 业务服务的协调配合。本文档详细梳理了从用户点击登录到最终完成认证的完整流程。
## 🏗️ 架构图
```mermaid
sequenceDiagram
participant U as 用户
participant F as 前端(Next.js)
participant G as Google OAuth
participant J as Java认证服务
participant P as Python业务服务
U->>F: 点击Google登录
F->>F: 生成state参数(包含邀请码)
F->>G: 重定向到Google授权页面
G->>U: 显示授权页面
U->>G: 确认授权
G->>F: 回调到/api/auth/google/callback
F->>F: API路由重定向到页面路由
F->>J: 调用Java验证接口
J->>G: 验证授权码获取用户信息
J->>F: 返回验证结果
F->>P: 调用Python注册接口
P->>F: 返回用户信息和token
F->>F: 保存用户信息到localStorage
F->>U: 跳转到主页面
```
## 🔧 核心组件分析
### 1. 环境配置 (.env.production)
<augment_code_snippet path=".env.production" mode="EXCERPT">
```bash
# Java认证服务
NEXT_PUBLIC_JAVA_URL = https://auth.test.movieflow.ai
# Python业务服务
NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com
# Google OAuth配置
NEXT_PUBLIC_GOOGLE_CLIENT_ID=847079918888-o1nne8d3ij80dn20qurivo987pv07225.apps.googleusercontent.com
NEXT_PUBLIC_GOOGLE_REDIRECT_URI=https://www.movieflow.net/api/auth/google/callback
```
</augment_code_snippet>
**关键问题**
- ❌ 域名不一致redirect_uri 使用 `movieflow.net`,但实际部署可能在 `movieflow.ai`
- ❌ 硬编码配置:多个环境的配置混杂在一起
### 2. Next.js 路由代理配置
<augment_code_snippet path="next.config.js" mode="EXCERPT">
```javascript
// Google OAuth2 API代理 (排除callback让Next.js本地处理)
{
source: '/api/auth/google/((?!callback).*)',
destination: `${AUTH_API_URL}/api/auth/google/$1`,
},
```
</augment_code_snippet>
**设计意图**
- ✅ 除了 callback 路由外,其他 Google OAuth API 都代理到 Java 服务
- ✅ callback 路由由 Next.js 本地处理,便于前端控制流程
### 3. 登录发起流程
#### 3.1 登录页面组件
<augment_code_snippet path="components/pages/login.tsx" mode="EXCERPT">
```typescript
const handleGoogleSignIn = async () => {
try {
setGoogleLoading(true)
setFormError('')
// 获取邀请码从URL参数或其他来源
const inviteCode = searchParams?.get('invite') || undefined
// 使用Google GSI SDK进行登录
await signInWithGoogle(inviteCode)
} catch (error: any) {
console.error('Google sign-in error:', error)
setFormError(error.message || 'Google sign-in failed, please try again')
setGoogleLoading(false)
}
}
```
</augment_code_snippet>
#### 3.2 Google 登录核心逻辑
<augment_code_snippet path="lib/auth.ts" mode="EXCERPT">
```typescript
export const signInWithGoogle = async (inviteCode?: string): Promise<void> => {
try {
// 从环境变量获取配置
const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID
const javaBaseUrl = process.env.NEXT_PUBLIC_JAVA_URL
const redirectUri = process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI
// 生成随机nonce用于安全验证
const nonce = Array.from(crypto.getRandomValues(new Uint8Array(32)))
.map((b) => b.toString(16).padStart(2, '0'))
.join('')
// 构建state参数包含邀请码等信息
const stateData = {
inviteCode: finalInviteCode || '',
timestamp: Date.now(),
origin: window.location.pathname + window.location.search,
nonce: nonce,
}
// 构建Google OAuth2授权URL
const authParams = new URLSearchParams({
access_type: 'online',
client_id: clientId,
nonce: nonce,
redirect_uri: redirectUri,
response_type: 'code', // 使用授权码模式
scope: 'email openid profile',
state: JSON.stringify(stateData),
prompt: 'select_account', // 总是显示账号选择
})
const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${authParams.toString()}`
// 保存state到sessionStorage用于验证
sessionStorage.setItem(
'google_oauth_state',
JSON.stringify({
nonce: nonce,
timestamp: Date.now(),
inviteCode: finalInviteCode || '',
})
)
// 直接在当前页面跳转到Google
window.location.href = authUrl
} catch (error) {
console.error('Google登录跳转失败:', error)
throw error
}
}
```
</augment_code_snippet>
**关键特性**
- ✅ 使用 `crypto.getRandomValues` 生成安全的 nonce
- ✅ state 参数包含邀请码、时间戳等信息
- ✅ 使用 sessionStorage 保存状态用于后续验证
- ❌ 硬编码的 redirect_uri 可能导致域名不匹配问题
### 4. 回调处理流程
#### 4.1 API 路由回调处理
<augment_code_snippet path="app/api/auth/google/callback/route.ts" mode="EXCERPT">
```typescript
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const code = searchParams.get('code')
const state = searchParams.get('state')
if (!code || !state) {
return NextResponse.json(
{
success: false,
message: 'Missing required parameters: code and state',
},
{ status: 400 }
)
}
// 重定向到页面路由让页面处理OAuth回调
const callbackUrl = `/users/oauth/callback?code=${encodeURIComponent(
code
)}&state=${encodeURIComponent(state)}`
// 修复:确保使用正确的域名进行重定向
const host = request.headers.get('host') || 'www.movieflow.net'
const protocol = request.headers.get('x-forwarded-proto') || 'https'
const fullCallbackUrl = `${protocol}://${host}${callbackUrl}`
return NextResponse.redirect(fullCallbackUrl)
}
```
</augment_code_snippet>
**设计模式**
- ✅ API 路由仅作为中转,将 GET 请求重定向到页面路由
- ✅ 动态构建重定向 URL避免硬编码域名
- ❌ 默认域名仍然是硬编码的 `movieflow.net`
#### 4.2 页面路由回调处理
<augment_code_snippet path="app/users/oauth/callback/page.tsx" mode="EXCERPT">
```typescript
const handleOAuthCallback = async () => {
try {
// 获取URL参数
const params: OAuthCallbackParams = {
code: searchParams.get('code') || undefined,
state: searchParams.get('state') || undefined,
error: searchParams.get('error') || undefined,
error_description: searchParams.get('error_description') || undefined,
}
// 解析state参数
let stateData: any = {}
if (params.state) {
try {
stateData = JSON.parse(params.state)
} catch (e) {
console.error('Failed to parse state parameter:', e)
}
}
// 第一步调用Java验证接口只验证不创建用户
const javaBaseUrl = 'https://auth.test.movieflow.ai'
const verifyResponse = await fetch(
`${javaBaseUrl}/api/auth/google/callback`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
code: params.code, // Google authorization code
state: params.state, // state参数
inviteCode: finalInviteCode, // 邀请码
skipUserCreation: true, // 🔑 关键:只验证不创建用户
}),
}
)
// 第二步调用Python注册接口进行用户创建和积分发放
const smartvideoBaseUrl = process.env.NEXT_PUBLIC_BASE_URL
const registerResponse = await fetch(
`${smartvideoBaseUrl}/api/user_fission/google_register`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
email: verifyResult.data.email,
name: verifyResult.data.name,
google_id: verifyResult.data.sub,
invite_code: finalInviteCode,
}),
}
)
// 保存用户信息到localStorage
const userData = {
userId: registerResult.data.user_id,
userName: registerResult.data.name,
name: registerResult.data.name,
email: registerResult.data.email,
authType: registerResult.data.auth_type || 'GOOGLE',
isNewUser: true,
inviteCode: registerResult.data.invite_code,
}
localStorage.setItem('currentUser', JSON.stringify(userData))
if (registerResult.data.token) {
localStorage.setItem('token', registerResult.data.token)
}
// 2秒后跳转到主页
setTimeout(() => {
const returnUrl = '/home'
window.location.href = returnUrl
}, 2000)
} catch (error) {
console.error('OAuth callback处理失败:', error)
setStatus('error')
setMessage(error.message || 'Authentication failed')
}
}
```
</augment_code_snippet>
**核心特性**
- ✅ 两步验证:先 Java 验证 Google Token再 Python 创建用户
- ✅ 支持邀请码逻辑
- ✅ 完整的错误处理和用户反馈
- ❌ 硬编码的 Java 服务地址
- ❌ 缺少 state 参数的安全验证
## 🚨 发现的问题
### 1. 域名配置不一致
- **问题**redirect_uri 配置为 `movieflow.net`,但实际可能部署在 `movieflow.ai`
- **影响**Google OAuth 回调失败
- **建议**:统一域名配置,使用环境变量管理
### 2. 硬编码配置过多
- **问题**Java 服务地址、域名等多处硬编码
- **影响**:环境切换困难,维护成本高
- **建议**:全部使用环境变量配置
### 3. 安全验证不完整
- **问题**:缺少 state 参数的 nonce 验证
- **影响**:存在 CSRF 攻击风险
- **建议**:完善 state 参数验证逻辑
### 4. 错误处理不够细致
- **问题**:某些错误场景处理不够详细
- **影响**:用户体验不佳,问题排查困难
- **建议**:增加更详细的错误分类和处理
## 📈 优化建议
### 1. 配置管理优化
```typescript
// 建议的配置管理方式
const config = {
googleClientId: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!,
javaBaseUrl: process.env.NEXT_PUBLIC_JAVA_URL!,
pythonBaseUrl: process.env.NEXT_PUBLIC_BASE_URL!,
redirectUri: process.env.NEXT_PUBLIC_GOOGLE_REDIRECT_URI!,
frontendUrl: process.env.NEXT_PUBLIC_FRONTEND_URL!,
}
```
### 2. 安全性增强
```typescript
// 建议的 state 验证逻辑
const validateState = (receivedState: string): boolean => {
try {
const stateData = JSON.parse(receivedState)
const storedState = sessionStorage.getItem('google_oauth_state')
if (!storedState) return false
const stored = JSON.parse(storedState)
// 验证 nonce 和时间戳
return (
stateData.nonce === stored.nonce &&
Math.abs(Date.now() - stored.timestamp) < 600000
) // 10分钟有效期
} catch {
return false
}
}
```
### 3. 错误处理优化
```typescript
// 建议的错误处理策略
enum OAuthError {
INVALID_STATE = 'INVALID_STATE',
EXPIRED_STATE = 'EXPIRED_STATE',
GOOGLE_AUTH_FAILED = 'GOOGLE_AUTH_FAILED',
USER_CREATION_FAILED = 'USER_CREATION_FAILED',
NETWORK_ERROR = 'NETWORK_ERROR',
}
const handleOAuthError = (error: OAuthError, details?: any) => {
const errorMessages = {
[OAuthError.INVALID_STATE]: '安全验证失败,请重新登录',
[OAuthError.EXPIRED_STATE]: '登录会话已过期,请重新登录',
[OAuthError.GOOGLE_AUTH_FAILED]: 'Google 认证失败,请重试',
[OAuthError.USER_CREATION_FAILED]: '用户创建失败,请联系客服',
[OAuthError.NETWORK_ERROR]: '网络连接失败,请检查网络后重试',
}
setMessage(errorMessages[error] || '未知错误')
console.error(`OAuth Error: ${error}`, details)
}
```
## 🔄 完整流程时序图
```mermaid
graph TD
A[用户点击Google登录] --> B[检查Google登录是否启用]
B --> C{启用状态}
C -->|启用| D[生成state参数和nonce]
C -->|未启用| E[隐藏Google登录按钮]
D --> F[保存state到sessionStorage]
F --> G[构建Google OAuth URL]
G --> H[跳转到Google授权页面]
H --> I[用户在Google页面授权]
I --> J[Google回调到/api/auth/google/callback]
J --> K[API路由获取code和state]
K --> L{参数验证}
L -->|失败| M[返回400错误]
L -->|成功| N[重定向到/users/oauth/callback页面]
N --> O[页面路由解析URL参数]
O --> P[解析state参数获取邀请码]
P --> Q[调用Java验证接口]
Q --> R{Java验证结果}
R -->|失败| S[显示错误信息]
R -->|成功| T[调用Python注册接口]
T --> U{Python注册结果}
U -->|失败| V[显示注册失败]
U -->|成功| W[保存用户信息到localStorage]
W --> X[显示成功信息]
X --> Y[2秒后跳转到/movies]
```
## 📊 数据流分析
### 1. State 参数结构
```typescript
interface StateData {
inviteCode: string // 邀请码
timestamp: number // 时间戳
origin: string // 原始页面路径
nonce: string // 安全随机数
}
```
### 2. Java 验证接口请求
```typescript
interface JavaVerifyRequest {
code: string // Google授权码
state: string // state参数
inviteCode?: string // 邀请码
skipUserCreation: true // 只验证不创建用户
}
```
### 3. Python 注册接口请求
```typescript
interface PythonRegisterRequest {
email: string // 用户邮箱
name: string // 用户姓名
google_id: string // Google用户ID
invite_code?: string // 邀请码
}
```
## 🛡️ 安全机制分析
### 1. CSRF 防护
- ✅ 使用 state 参数防止 CSRF 攻击
- ✅ 生成随机 nonce 增强安全性
- ❌ 缺少 state 参数的服务端验证
### 2. 会话管理
- ✅ 使用 sessionStorage 临时存储状态
- ✅ 设置时间戳用于过期检查
- ❌ 缺少自动清理过期状态的机制
### 3. Token 安全
- ✅ 使用授权码模式,不直接暴露 access_token
- ✅ Token 存储在 localStorage
- ❌ 缺少 Token 过期自动刷新机制
## 🔧 技术债务清单
### 高优先级
1. **域名配置统一**
- 问题redirect_uri 域名不匹配
- 影响OAuth 回调失败
- 工作量1 天
2. **硬编码配置清理**
- 问题:多处硬编码服务地址
- 影响:环境切换困难
- 工作量0.5 天
### 中优先级
3. **State 参数验证完善**
- 问题:缺少 nonce 验证
- 影响:安全风险
- 工作量1 天
4. **错误处理优化**
- 问题:错误分类不够细致
- 影响:用户体验和调试困难
- 工作量1 天
### 低优先级
5. **Token 刷新机制**
- 问题:缺少自动刷新
- 影响:用户需要重新登录
- 工作量2 天
6. **日志和监控完善**
- 问题:缺少结构化日志
- 影响:问题排查困难
- 工作量1 天
## 🎯 总结
Video Flow 的 Google OAuth 回调逻辑整体设计合理,采用了前后端分离的架构,支持邀请码功能,具备基本的安全防护。主要问题集中在配置管理和安全验证的细节上。
**优势**
- ✅ 架构清晰,职责分离
- ✅ 支持邀请码业务逻辑
- ✅ 具备基本的错误处理
- ✅ 使用标准的 OAuth 2.0 授权码模式
**待改进**
- ❌ 配置管理需要优化
- ❌ 安全验证需要加强
- ❌ 错误处理需要细化
- ❌ 监控和日志需要完善
建议按照技术债务清单的优先级逐步改进,优先解决影响功能正常运行的配置问题,然后逐步完善安全性和用户体验。