Files
raisfast/docs/google-oauth-setup.md
T

495 lines
13 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 OAuth 接入详细步骤
## 1. 创建 Google Cloud 项目
### 1.1 进入 Google Cloud Console
1. 打开 https://console.cloud.google.com/
2. 使用 Google 账号登录
### 1.2 创建新项目
1. 点击顶部导航栏的项目选择器
2. 点击 **NEW PROJECT**
3. 填写:
- Project name: `raisfast`
- Organization: 保持默认
4. 点击 **CREATE**
5. 等待项目创建完成,选择该项目
## 2. 配置 OAuth 同意屏幕
> Google 要求所有 OAuth 应用必须配置同意屏幕,否则无法使用。
### 2.1 进入配置页面
1. 左侧菜单 → **APIs & Services****OAuth consent screen**
2. 直接链接:https://console.cloud.google.com/apis/credentials/consent
### 2.2 选择用户类型
| 用户类型 | 适用场景 |
|---------|---------|
| External | 任何 Google 账号都可登录(推荐) |
| Internal | 仅组织内 G Suite 账号可登录 |
选择 **External** → 点击 **CREATE**
### 2.3 填写应用信息
**OAuth consent screen 页面:**
| 字段 | 值 |
|------|-----|
| App name | `raisfast` |
| User support email | 你的邮箱 |
| App logo | (可选,后续上传) |
**App domain(全部可选):**
| 字段 | 值 |
|------|-----|
| Application home page | `http://localhost:3000` |
| Application privacy policy link | (可选) |
| Application terms of service link | (可选) |
**Authorized domains**
点击 **ADD DOMAIN**,添加:
| 域名 |
|------|
| `localhost` |
> 生产环境添加你的域名如 `yourdomain.com`
**Developer contact information**
| 字段 | 值 |
|------|-----|
| Email addresses | 你的邮箱 |
点击 **SAVE AND CONTINUE**
### 2.4 配置 Scopes
1. 点击 **ADD OR REMOVE SCOPES**
2. 搜索并勾选以下 scope
| Scope | 说明 |
|-------|------|
| `.../auth/userinfo.email` | 读取用户邮箱 |
| `.../auth/userinfo.profile` | 读取用户基本信息(名字、头像) |
| `openid` | OpenID Connect |
3. 点击 **UPDATE****SAVE AND CONTINUE**
### 2.5 添加测试用户(发布前必须)
如果应用状态为 **Testing**,只有添加的测试用户可以登录。
1. 点击 **ADD USERS**
2. 输入你的 Gmail 邮箱
3. 点击 **ADD****SAVE AND CONTINUE**
### 2.6 发布应用(可选)
Testing 状态下只有测试用户能登录。如需所有人可登录:
1. 回到 OAuth consent screen
2. 点击 **PUBLISH APP**
3. 确认发布
> **注意**:发布后如果请求敏感 scope 可能需要 Google 审核。`email` + `profile` 是非敏感的,无需审核。
## 3. 创建 OAuth 2.0 凭证
### 3.1 进入凭证页面
1. 左侧菜单 → **APIs & Services****Credentials**
2. 直接链接:https://console.cloud.google.com/apis/credentials
### 3.2 创建 OAuth 客户端
1. 点击顶部 **+ CREATE CREDENTIALS** → **OAuth client ID**
2. 填写:
| 字段 | 值 |
|------|-----|
| Application type | **Web application** |
| Name | `raisfast-web` |
**Authorized JavaScript origins**
点击 **ADD URI**,添加:
| 环境 | URI |
|------|-----|
| 本地开发 | `http://localhost:9000` |
| 生产环境 | `https://yourdomain.com` |
**Authorized redirect URIs**
点击 **ADD URI**,添加:
| 环境 | URI |
|------|-----|
| 本地开发 | `http://localhost:9000/api/v1/auth/oauth/google/callback` |
| 生产环境 | `https://yourdomain.com/api/v1/auth/oauth/google/callback` |
3. 点击 **CREATE**
### 3.3 获取凭证
创建完成后弹出窗口显示:
- **Your Client ID**:形如 `123456789-abcxxx.apps.googleusercontent.com`
- **Your Client Secret**:形如 `GOCSPX-xxxxxxxxxx`
点击 **DOWNLOAD JSON** 备份凭证(可选)。
> Client Secret 可以在凭证详情页随时查看,不会像 GitHub 那样只显示一次。
## 4. 后端代码实现
Google OAuth 需要新增 Provider 实现。创建 `src/oauth/google.rs`
### 4.1 Google Provider 实现
Google 的 OAuth2 端点:
| 端点 | URL |
|------|-----|
| Authorization | `https://accounts.google.com/o/oauth2/v2/auth` |
| Token Exchange | `https://oauth2.googleapis.com/token` |
| User Info | `https://www.googleapis.com/oauth2/v2/userinfo` |
### 4.2 关键差异(对比 GitHub
| 对比项 | GitHub | Google |
|--------|--------|--------|
| Scope | `user:email` | `openid email profile` |
| Token 响应 | `access_token` 在 JSON body | 同 |
| 用户信息 API | `GET /user` + `GET /user/emails` | `GET /userinfo`(一次请求) |
| 用户 ID | 数字字符串 | 数字字符串(`sub` 字段) |
| 邮箱 | 可能需要额外请求 | 直接返回,有 `verified_email` 字段 |
### 4.3 实现步骤
#### 步骤一:创建 Google Provider 文件
`src/oauth.rs` 中添加 Google Provider(与 GitHub 同文件):
```rust
// 在 src/oauth.rs 末尾添加
// ── Google Provider ────────────────────────────────────────────
pub struct GoogleProvider {
client_id: String,
client_secret: String,
}
impl GoogleProvider {
pub fn new(client_id: String, client_secret: String) -> Self {
Self { client_id, client_secret }
}
}
#[async_trait::async_trait]
impl OAuthProvider for GoogleProvider {
fn name(&self) -> &str {
"google"
}
fn authorize_url(&self, state: &str, code_challenge: &str) -> String {
format!(
"https://accounts.google.com/o/oauth2/v2/auth?client_id={}&state={}&code_challenge={}&code_challenge_method=S256&scope=openid+email+profile&response_type=code&access_type=offline",
self.client_id, state, code_challenge
)
}
async fn exchange_code(&self, code: &str, code_verifier: &str)
-> AppResult<OAuthTokenResponse>
{
let client = reqwest::Client::new();
let resp = client
.post("https://oauth2.googleapis.com/token")
.header("Accept", "application/json")
.json(&serde_json::json!({
"client_id": self.client_id,
"client_secret": self.client_secret,
"code": code,
"code_verifier": code_verifier,
"grant_type": "authorization_code",
"redirect_uri": "", // PKCE 模式不需要
}))
.send()
.await
.map_err(|e| AppError::Internal(
anyhow::anyhow!("Google token exchange failed: {e}")
))?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(AppError::Internal(anyhow::anyhow!(
"Google token exchange returned {status}: {body}"
)));
}
resp.json::<OAuthTokenResponse>().await.map_err(|e| {
AppError::Internal(anyhow::anyhow!(
"Google token response parse failed: {e}"
))
})
}
async fn fetch_user_info(&self, access_token: &str)
-> AppResult<OAuthUserInfo>
{
let client = reqwest::Client::new();
let resp = client
.get("https://www.googleapis.com/oauth2/v2/userinfo")
.header("Authorization", format!("Bearer {access_token}"))
.header("Accept", "application/json")
.send()
.await
.map_err(|e| AppError::Internal(
anyhow::anyhow!("Google user info request failed: {e}")
))?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
return Err(AppError::Internal(anyhow::anyhow!(
"Google user info returned {status}: {body}"
)));
}
let profile: serde_json::Value = resp.json().await.map_err(|e| {
AppError::Internal(anyhow::anyhow!(
"Google user info parse failed: {e}"
))
})?;
let provider_user_id = profile["sub"]
.as_str()
.unwrap_or_default()
.to_string();
let email = profile["email"].as_str().map(|s| s.to_string());
let display_name = profile["name"].as_str()
.or_else(|| profile["given_name"].as_str())
.map(|s| s.to_string());
let avatar_url = profile["picture"].as_str().map(|s| s.to_string());
Ok(OAuthUserInfo {
provider_user_id,
email,
display_name,
avatar_url,
raw_profile: profile,
})
}
}
```
#### 步骤二:在 server.rs 注册 Google Provider
`build_oauth_registry` 函数中添加:
```rust
if let Some(google) = &config.oauth.google {
registry.register(Box::new(
crate::oauth::GoogleProvider::new(
google.client_id.clone(),
google.client_secret.clone(),
),
));
tracing::info!("OAuth provider registered: google");
}
```
#### 步骤三:更新用户名生成策略
`ensure_unique_username` 中的 fallback 前缀需要支持 google
```rust
async fn ensure_unique_username(pool: &crate::db::Pool, base: &str, provider: &str) -> AppResult<String> {
// ...
let prefixed = format!("{provider}_{username}");
// ...
}
```
## 5. 配置环境变量
编辑 `.env`
```env
# Google OAuth
OAUTH_GOOGLE_CLIENT_ID=123456789-abcxxx.apps.googleusercontent.com
OAUTH_GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxx
```
> `OAUTH_ENABLED=true` 已在 GitHub 配置时设置,无需重复。
## 6. 验证配置
重启后端,检查日志:
```
OAuth provider registered: github
OAuth provider registered: google
```
测试 API
```bash
# 查看已配置 Provider
curl http://localhost:9000/api/v1/auth/oauth/providers
# 预期返回:
# {"code":0,"message":"操作成功","data":[
# {"name":"github","configured":true},
# {"name":"google","configured":true}
# ]}
# 测试 Google 授权 URL
curl -v http://localhost:9000/api/v1/auth/oauth/google 2>&1 | grep "Location:"
# 应输出:
# Location: https://accounts.google.com/o/oauth2/v2/auth?client_id=...
```
## 7. 测试完整流程
### 7.1 发起 Google 登录
浏览器打开:
```
http://localhost:9000/api/v1/auth/oauth/google
```
预期:302 到 Google 登录页面
### 7.2 Google 授权
1. 选择 Google 账号
2. 如果是 Testing 模式,会看到 "This app isn't verified" 警告
3. 点击 **Advanced****Go to raisfast (unsafe)**
4. 点击 **Continue** 授权
### 7.3 回调处理
Google 回调到:
```
http://localhost:9000/api/v1/auth/oauth/google/callback?code=xxx&state=xxx&scope=email+profile+openid
```
后端处理后 302 到前端:
```
http://localhost:3000/auth/callback?access_token=...&refresh_token=...&expires_in=900
```
### 7.4 前端登录页添加 Google 按钮
```tsx
<a href={`${API_BASE}/auth/oauth/google`}>
<GoogleIcon />
Google 登录
</a>
```
## 8. 常见问题
### "This app isn't verified" 警告
**原因**:应用处于 Testing 状态,且请求了非敏感 scope。
**解决(开发阶段)**
1. 确保你的邮箱已添加到 Test Users
2. 点击 **Advanced****Go to raisfast (unsafe)** 即可继续
**解决(生产阶段)**
1. 在 OAuth consent screen 页面点击 **PUBLISH APP**
### Error 400: redirect_uri_mismatch
**原因**Google OAuth App 中配置的 redirect URI 与实际不匹配。
**检查**
1. Google Console → Credentials → 你的 OAuth client → Authorized redirect URIs
2. 确保包含 `http://localhost:9000/api/v1/auth/oauth/google/callback`
3. 注意尾部斜杠、协议、端口号必须完全一致
### Error 401: invalid_client
**原因**Client ID 或 Client Secret 不正确。
**解决**
1. 复制 Google Console 中的完整 Client ID(很长的字符串)
2. 确认 `.env` 中没有多余空格或换行
### 获取不到邮箱
**检查**
1. OAuth consent screen 中是否添加了 `email` scope
2. 授权 URL 中 `scope=openid+email+profile` 是否完整
3. Google 账号是否设置了邮箱
### 用户名冲突
如果 Google 用户名与已有用户冲突,系统自动追加前缀:
```
alice → google_alice → google_alice_a3f8
```
## 9. 生产环境部署
### 9.1 更新 Google OAuth App
| 配置项 | 添加值 |
|--------|--------|
| Authorized JavaScript origins | `https://yourdomain.com` |
| Authorized redirect URIs | `https://yourdomain.com/api/v1/auth/oauth/google/callback` |
### 9.2 发布应用
1. OAuth consent screen → **PUBLISH APP**
2. 如果 scope 中包含敏感 scope,需要提交 Google 审核(通常 1-3 天)
### 9.3 隐私政策页面
Google 要求应用提供隐私政策链接:
1. 在前端创建 `/privacy` 页面
2. 在 OAuth consent screen 中添加 Privacy policy URL
### 9.4 环境变量
```env
OAUTH_GOOGLE_CLIENT_ID=your-production-client-id
OAUTH_GOOGLE_CLIENT_SECRET=your-production-client-secret
```
## 10. Google vs GitHub 对比
| 对比项 | GitHub | Google |
|--------|--------|--------|
| 创建凭证 | Settings → Developer settings | Google Cloud Console |
| 凭证数量 | Client ID + Secret(同页面) | Client ID + Secret(同页面) |
| 回调 URL 配置 | 创建时设置,可修改 | 创建时设置,可修改 |
| Scope | `user:email` | `openid email profile` |
| 邮箱获取 | 可能需要额外 API 调用 | 直接返回 |
| 用户唯一标识 | `id`(数字) | `sub`(数字) |
| 头像 | `avatar_url` | `picture` |
| 昵称 | `login` | `name``given_name` |
| 应用审核 | 不需要 | 敏感 scope 需要审核 |
| 测试模式 | 无 | Testing 模式限制用户 |
| Consent screen | 简单 | 复杂(需完整配置) |