video-flow-b/docs/google-oauth-implementation.md
2025-10-27 15:48:42 +08:00

774 lines
17 KiB
Markdown
Raw 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 OAuth2 集成实现文档
## 📋 概述
本文档详细记录了 video-flow-b 项目中 Google OAuth2 登录功能的完整实现,包括前端 UI 集成、后端 API 对接、错误处理和安全机制等。
**实现日期:** 2024 年 12 月
**涉及功能:**
- Google 登录按钮集成
- OAuth2 授权流程
- 邮箱冲突处理
- 账户绑定功能
- 安全防护机制
---
## 🏗️ 架构设计
### 整体流程图
```mermaid
graph TD
A[用户点击Google登录] --> B[获取授权URL]
B --> C[重定向到Google]
C --> D[用户授权]
D --> E[回调处理]
E --> F{邮箱冲突?}
F -->|否| G[登录成功]
F -->|是| H[显示冲突处理]
H --> I[绑定账户]
H --> J[返回登录]
I --> G
```
### 技术栈
- **前端框架**: Next.js 14 + React 18
- **类型系统**: TypeScript
- **UI 组件**: 自定义组件 + Tailwind CSS
- **状态管理**: React Hooks + localStorage
- **HTTP 客户端**: Fetch API
- **安全机制**: CSRF 防护 + 会话管理
---
## 📁 文件结构
```
├── app/
│ ├── types/
│ │ └── google-oauth.ts # OAuth类型定义
│ ├── users/oauth/callback/
│ │ └── page.tsx # OAuth回调页面
│ └── signup/
│ └── page.tsx # 注册页面(已更新)
├── components/
│ ├── auth/
│ │ └── email-conflict-modal.tsx # 邮箱冲突处理组件
│ ├── pages/
│ │ └── login.tsx # 登录页面(已更新)
│ └── ui/
│ └── google-login-button.tsx # Google登录按钮组件
├── lib/
│ └── auth.ts # 认证核心逻辑(已更新)
└── docs/
├── google.md # API接口文档
└── google-oauth-implementation.md # 本文档
```
---
## 🔧 核心实现
### 1. TypeScript 类型定义
**文件**: `app/types/google-oauth.ts`
定义了完整的 OAuth 相关类型:
```typescript
// 基础响应接口
export interface BaseResponse<T = any> {
success: boolean
data?: T
error?: string
message?: string
}
// Google用户信息
export interface GoogleUserInfo {
userId: string
userName: string
name: string
email: string
authType: 'GOOGLE' | 'LOCAL'
url?: string
isNewUser: boolean
}
// 邮箱冲突数据
export interface EmailConflictData {
bindToken: string
existingUser: {
email: string
authType: 'LOCAL' | 'GOOGLE'
}
}
```
### 2. 认证核心逻辑
**文件**: `lib/auth.ts`
#### 2.1 OAuth 授权流程
```typescript
export const signInWithGoogle = async () => {
try {
// 获取授权URL
const response = await fetch(`${JAVA_BASE_URL}/api/auth/google/authorize`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
})
const data: BaseResponse<GoogleAuthorizeResponse> = await response.json()
if (data.success && data.data) {
// 保存state参数用于CSRF防护
const oauthState: OAuthState = {
state: data.data.state,
timestamp: Date.now(),
redirectUrl: window.location.pathname,
}
sessionStorage.setItem('google_oauth_state', JSON.stringify(oauthState))
// 重定向到Google授权页面
window.location.href = data.data.authUrl
}
} catch (error) {
console.error('Google OAuth initialization failed:', error)
throw error
}
}
```
#### 2.2 ID Token 登录
```typescript
export const loginWithGoogleToken = async (
idToken: string,
action: 'login' | 'register' | 'auto' = 'auto'
) => {
try {
const response = await fetch(`${JAVA_BASE_URL}/api/auth/google/login`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ idToken, action }),
})
const data: BaseResponse<GoogleLoginSuccessResponse> = await response.json()
// 处理邮箱冲突
if (response.status === 409) {
const conflictData = data as unknown as BaseResponse<EmailConflictData>
throw {
type: 'EMAIL_CONFLICT',
status: 409,
data: conflictData.data,
message: data.message || 'Email already exists',
}
}
if (!data.success || !data.data) {
throw new Error(data.message || 'Google login failed')
}
// 保存认证信息
setToken(data.data.token)
setUser(data.data.userInfo)
return data.data.userInfo
} catch (error) {
console.error('Google token login failed:', error)
throw error
}
}
```
#### 2.3 账户绑定
```typescript
export const bindGoogleAccount = async (
bindToken: string,
idToken?: string
) => {
try {
const token = getToken()
if (!token) {
throw new Error('User not authenticated')
}
const response = await fetch(`${JAVA_BASE_URL}/api/auth/google/bind`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
bindToken,
idToken,
confirm: true,
}),
})
const data: BaseResponse<{ token: string; message: string }> =
await response.json()
if (!data.success || !data.data) {
throw new Error(data.message || 'Account binding failed')
}
// 更新token
setToken(data.data.token)
return data.data
} catch (error) {
console.error('Google account binding failed:', error)
throw error
}
}
```
### 3. UI 组件集成
#### 3.1 登录页面更新
**文件**: `components/pages/login.tsx`
**主要更改:**
- 添加 GoogleLoginButton 组件导入
- 添加 googleLoading 状态管理
- 实现 async/await 的 handleGoogleSignIn 函数
- 添加 OAuth 回调错误处理
```typescript
const handleGoogleSignIn = async () => {
try {
setGoogleLoading(true)
setFormError('')
await signInWithGoogle()
} catch (error: any) {
console.error('Google sign-in error:', error)
setFormError(error.message || 'Google sign-in failed, please try again')
setGoogleLoading(false)
}
}
```
#### 3.2 注册页面更新
**文件**: `app/signup/page.tsx`
**主要更改:**
- 取消注释 GoogleLoginButton 组件
- 添加分隔线保持 UI 一致性
- 更新 handleGoogleSignIn 函数支持 async/await
### 4. OAuth 回调处理
**文件**: `app/users/oauth/callback/page.tsx`
完整的 OAuth 回调处理页面,包含:
#### 4.1 安全验证
```typescript
// 验证state参数防止CSRF攻击
const savedStateStr = sessionStorage.getItem('google_oauth_state')
if (!savedStateStr) {
setStatus('error')
setMessage('OAuth state not found. Please try again.')
return
}
const savedState: OAuthState = JSON.parse(savedStateStr)
if (savedState.state !== params.state) {
setStatus('error')
setMessage('Invalid OAuth state. Possible CSRF attack detected.')
return
}
// 检查state是否过期5分钟
if (Date.now() - savedState.timestamp > 5 * 60 * 1000) {
setStatus('error')
setMessage('OAuth session expired. Please try again.')
return
}
```
#### 4.2 状态管理
支持四种状态:
- `loading`: 处理中
- `success`: 登录成功
- `error`: 登录失败
- `conflict`: 邮箱冲突
### 5. 邮箱冲突处理
**文件**: `components/auth/email-conflict-modal.tsx`
专门处理 HTTP 409 邮箱冲突响应的模态框组件:
```typescript
const handleBindAccount = async () => {
try {
setIsBinding(true)
setError('')
await onBindAccount(conflictData.bindToken)
onClose()
} catch (error: any) {
console.error('Account binding failed:', error)
setError(error.message || 'Account binding failed')
} finally {
setIsBinding(false)
}
}
```
---
## 🔒 安全机制
### 1. CSRF 防护
- 使用 OAuth state 参数防止 CSRF 攻击
- state 参数存储在 sessionStorage 中
- 回调时验证 state 参数一致性
### 2. 会话管理
- OAuth state 有效期为 5 分钟
- 超时自动清理会话数据
- 支持会话状态检查
### 3. 错误处理
- 完整的错误类型定义
- 分层错误处理机制
- 用户友好的错误提示
### 4. 数据验证
- TypeScript 类型安全
- API 响应数据验证
- 参数完整性检查
---
## 📊 API 接口对应
| API 接口 | 实现函数 | 状态 | 说明 |
| -------------------------------- | ------------------------ | ---- | ---------------- |
| `GET /api/auth/google/authorize` | `signInWithGoogle()` | ✅ | 获取授权 URL |
| `POST /api/auth/google/login` | `loginWithGoogleToken()` | ✅ | ID Token 登录 |
| `POST /api/auth/google/bind` | `bindGoogleAccount()` | ✅ | 绑定 Google 账户 |
| `GET /api/auth/google/status` | `getGoogleBindStatus()` | ✅ | 查询绑定状态 |
---
## 🎯 用户流程
### 正常登录流程
1. 用户点击 Google 登录按钮
2. 系统调用`/api/auth/google/authorize`获取授权 URL
3. 重定向到 Google OAuth 页面
4. 用户完成授权
5. 回调到`/users/oauth/callback`
6. 验证 state 参数和处理授权码
7. 登录成功,跳转到主页面
### 邮箱冲突处理流程
1. 用户使用 Google 登录
2. 后端返回 HTTP 409 状态码
3. 前端显示邮箱冲突模态框
4. 用户选择绑定账户或返回登录
5. 如选择绑定,调用绑定 API
6. 绑定成功后完成登录
---
## 🚀 部署注意事项
### 1. 环境变量配置
确保以下环境变量正确配置:
- `NEXT_PUBLIC_JAVA_URL`: Java 后端 API 基础 URL (默认: `https://77.app.java.auth.qikongjian.com`)
- Google OAuth 客户端 ID 和密钥
**重要说明**: 所有 Google OAuth 相关的 API 接口都使用`JAVA_BASE_URL`,与普通登录/注册接口保持一致。
### 2. 回调 URL 配置
在 Google Cloud Console 中配置正确的回调 URL
- 开发环境: `http://localhost:3000/users/oauth/callback`
- 生产环境: `https://yourdomain.com/users/oauth/callback`
### 3. HTTPS 要求
生产环境必须使用 HTTPS 协议,确保 OAuth 安全性。
---
## 🔍 测试建议
### 1. 功能测试
- [ ] Google 登录按钮显示正常
- [ ] OAuth 授权流程完整
- [ ] 邮箱冲突处理正确
- [ ] 账户绑定功能正常
- [ ] 错误处理机制有效
### 2. 安全测试
- [ ] CSRF 攻击防护
- [ ] 会话超时处理
- [ ] 参数验证完整
- [ ] 错误信息不泄露敏感数据
### 3. 兼容性测试
- [ ] 不同浏览器兼容性
- [ ] 移动端响应式设计
- [ ] 网络异常处理
---
## 📝 维护说明
### 1. 日志监控
建议监控以下关键指标:
- OAuth 授权成功率
- 邮箱冲突发生频率
- 错误类型分布
- 用户转化率
### 2. 定期更新
- 定期更新 Google OAuth SDK
- 检查安全漏洞和补丁
- 优化用户体验
### 3. 故障排查
常见问题及解决方案:
- OAuth 回调失败:检查回调 URL 配置
- CSRF 验证失败:检查 state 参数处理
- 邮箱冲突处理异常:检查绑定 API 实现
---
## 💡 使用示例
### 1. 在新页面中集成 Google 登录
```typescript
import { GoogleLoginButton } from '@/components/ui/google-login-button'
import { signInWithGoogle } from '@/lib/auth'
const MyPage = () => {
const [googleLoading, setGoogleLoading] = useState(false)
const [error, setError] = useState('')
const handleGoogleSignIn = async () => {
try {
setGoogleLoading(true)
setError('')
await signInWithGoogle()
} catch (error: any) {
console.error('Google sign-in error:', error)
setError(error.message || 'Google sign-in failed')
setGoogleLoading(false)
}
}
return (
<div>
{error && <div className="error">{error}</div>}
<GoogleLoginButton
onClick={handleGoogleSignIn}
loading={googleLoading}
variant="outline"
size="md"
className="w-full"
/>
</div>
)
}
```
### 2. 处理邮箱冲突
```typescript
import { EmailConflictModal } from '@/components/auth/email-conflict-modal'
import { bindGoogleAccount } from '@/lib/auth'
const handleEmailConflict = async (conflictData) => {
const [showConflictModal, setShowConflictModal] = useState(true)
const handleBindAccount = async (bindToken: string) => {
try {
await bindGoogleAccount(bindToken)
// 绑定成功,刷新页面或跳转
window.location.reload()
} catch (error) {
console.error('Binding failed:', error)
throw error
}
}
return (
<EmailConflictModal
isOpen={showConflictModal}
onClose={() => setShowConflictModal(false)}
conflictData={conflictData}
onBindAccount={handleBindAccount}
onReturnToLogin={() => router.push('/login')}
/>
)
}
```
### 3. 检查 Google 账户绑定状态
```typescript
import { getGoogleBindStatus } from '@/lib/auth'
const checkBindStatus = async () => {
try {
const status = await getGoogleBindStatus()
console.log('Google account bound:', status.isBound)
return status.isBound
} catch (error) {
console.error('Failed to check bind status:', error)
return false
}
}
```
---
## 🐛 常见问题与解决方案
### Q1: Google 登录按钮点击后没有反应
**可能原因:**
- 网络连接问题
- API 端点配置错误
- 环境变量未正确设置
**解决方案:**
1. 检查浏览器控制台错误信息
2. 验证`NEXT_PUBLIC_BASE_URL`环境变量
3. 确认后端 API 服务正常运行
### Q2: OAuth 回调页面显示"Invalid OAuth state"
**可能原因:**
- sessionStorage 被清除
- 多标签页操作冲突
- 会话超时
**解决方案:**
1. 确保在同一浏览器标签页完成 OAuth 流程
2. 检查会话超时设置(当前为 5 分钟)
3. 清除浏览器缓存后重试
### Q3: 邮箱冲突模态框不显示
**可能原因:**
- HTTP 状态码处理错误
- 类型转换问题
- 组件导入错误
**解决方案:**
1. 检查 API 返回的 HTTP 状态码是否为 409
2. 验证 EmailConflictModal 组件导入
3. 确认 conflictData 数据结构正确
### Q4: 账户绑定失败
**可能原因:**
- 用户未登录
- bindToken 过期
- 后端 API 错误
**解决方案:**
1. 确认用户已登录且 token 有效
2. 检查 bindToken 是否在有效期内
3. 查看后端 API 日志排查问题
---
## 🔧 自定义配置
### 1. 修改 OAuth 会话超时时间
`app/users/oauth/callback/page.tsx`中修改:
```typescript
// 当前为5分钟可根据需要调整
if (Date.now() - savedState.timestamp > 5 * 60 * 1000) {
// 改为10分钟
if (Date.now() - savedState.timestamp > 10 * 60 * 1000) {
```
### 2. 自定义 Google 登录按钮样式
```typescript
<GoogleLoginButton
onClick={handleGoogleSignIn}
loading={googleLoading}
variant="default" // 或 "outline"
size="lg" // "sm", "md", "lg"
className="custom-google-btn"
/>
```
### 3. 添加自定义错误处理
```typescript
const handleGoogleSignIn = async () => {
try {
await signInWithGoogle()
} catch (error: any) {
// 自定义错误处理逻辑
if (error.message.includes('network')) {
setError('网络连接失败,请检查网络后重试')
} else if (error.message.includes('timeout')) {
setError('请求超时,请重试')
} else {
setError('Google登录失败请稍后重试')
}
}
}
```
---
## 📈 性能优化建议
### 1. 代码分割
```typescript
// 懒加载邮箱冲突模态框
const EmailConflictModal = lazy(() =>
import('@/components/auth/email-conflict-modal').then((module) => ({
default: module.EmailConflictModal,
}))
)
```
### 2. 缓存优化
```typescript
// 缓存Google绑定状态
const useGoogleBindStatus = () => {
const [status, setStatus] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const cachedStatus = localStorage.getItem('google_bind_status')
if (cachedStatus) {
setStatus(JSON.parse(cachedStatus))
setLoading(false)
}
getGoogleBindStatus().then((result) => {
setStatus(result)
localStorage.setItem('google_bind_status', JSON.stringify(result))
setLoading(false)
})
}, [])
return { status, loading }
}
```
### 3. 错误重试机制
```typescript
const signInWithGoogleRetry = async (maxRetries = 3) => {
for (let i = 0; i < maxRetries; i++) {
try {
await signInWithGoogle()
return
} catch (error) {
if (i === maxRetries - 1) throw error
await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)))
}
}
}
```
---
## 👥 贡献者
- **开发者**: Augment Agent
- **审核者**: 待定
- **测试者**: 待定
---
## 📚 相关文档
- [Google OAuth2 API 文档](./google.md)
- [项目认证系统架构分析](./authentication-architecture-analysis.md)
- [前端组件设计规范](./frontend-component-guidelines.md)
---
## 📋 更新日志
### v1.0.1 (2024-12-19)
- 🔧 修正 Google OAuth API 接口基础 URL 配置
- 🔧 统一所有 Google OAuth 相关接口使用`JAVA_BASE_URL`
- 🔧 修正`getUserProfile`函数的 API 基础 URL
- 📝 更新文档中的 API 配置说明
### v1.0.0 (2024-12-19)
- ✅ 完成 Google 登录按钮集成
- ✅ 实现完整的 OAuth2 授权流程
- ✅ 添加邮箱冲突处理机制
- ✅ 实现账户绑定功能
- ✅ 完善安全防护机制
- ✅ 添加完整的错误处理
- ✅ 创建详细的技术文档
---
**最后更新**: 2024 年 12 月 19 日
**版本**: v1.0.0
**维护者**: video-flow-b 开发团队