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

13 KiB
Raw Blame History

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
  1. 点击 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
  1. 点击 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 同文件):

// 在 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 函数中添加:

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:

async fn ensure_unique_username(pool: &crate::db::Pool, base: &str, provider: &str) -> AppResult<String> {
    // ...
    let prefixed = format!("{provider}_{username}");
    // ...
}

5. 配置环境变量

编辑 .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:

# 查看已配置 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 按钮

<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 环境变量

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 简单 复杂(需完整配置)