15 KiB
🔍 Google OAuth 回调逻辑全面梳理
📋 概述
Video Flow 项目的 Google OAuth 登录采用了授权码模式,整个流程涉及前端、Java 认证服务和 Python 业务服务的协调配合。本文档详细梳理了从用户点击登录到最终完成认证的完整流程。
🏗️ 架构图
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">
# 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">
// 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">
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">
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">
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">
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 = '/movies'
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. 配置管理优化
// 建议的配置管理方式
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. 安全性增强
// 建议的 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. 错误处理优化
// 建议的错误处理策略
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)
}
🔄 完整流程时序图
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 参数结构
interface StateData {
inviteCode: string // 邀请码
timestamp: number // 时间戳
origin: string // 原始页面路径
nonce: string // 安全随机数
}
2. Java 验证接口请求
interface JavaVerifyRequest {
code: string // Google授权码
state: string // state参数
inviteCode?: string // 邀请码
skipUserCreation: true // 只验证不创建用户
}
3. Python 注册接口请求
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 过期自动刷新机制
🔧 技术债务清单
高优先级
-
域名配置统一
- 问题:redirect_uri 域名不匹配
- 影响:OAuth 回调失败
- 工作量:1 天
-
硬编码配置清理
- 问题:多处硬编码服务地址
- 影响:环境切换困难
- 工作量:0.5 天
中优先级
-
State 参数验证完善
- 问题:缺少 nonce 验证
- 影响:安全风险
- 工作量:1 天
-
错误处理优化
- 问题:错误分类不够细致
- 影响:用户体验和调试困难
- 工作量:1 天
低优先级
-
Token 刷新机制
- 问题:缺少自动刷新
- 影响:用户需要重新登录
- 工作量:2 天
-
日志和监控完善
- 问题:缺少结构化日志
- 影响:问题排查困难
- 工作量:1 天
🎯 总结
Video Flow 的 Google OAuth 回调逻辑整体设计合理,采用了前后端分离的架构,支持邀请码功能,具备基本的安全防护。主要问题集中在配置管理和安全验证的细节上。
优势:
- ✅ 架构清晰,职责分离
- ✅ 支持邀请码业务逻辑
- ✅ 具备基本的错误处理
- ✅ 使用标准的 OAuth 2.0 授权码模式
待改进:
- ❌ 配置管理需要优化
- ❌ 安全验证需要加强
- ❌ 错误处理需要细化
- ❌ 监控和日志需要完善
建议按照技术债务清单的优先级逐步改进,优先解决影响功能正常运行的配置问题,然后逐步完善安全性和用户体验。