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

12 KiB
Raw Blame History

GitHub OAuth 接入详细步骤

1. 创建 GitHub OAuth App

1.1 进入 GitHub 开发者设置

  1. 登录 GitHub
  2. 进入 SettingsDeveloper settingsOAuth Apps
  3. 直接链接:https://github.com/settings/developers

1.2 注册新应用

点击 New OAuth App,填写以下信息:

字段 本地开发值 生产环境值
Application name raisfast-dev 你的产品名
Homepage URL http://localhost:3000 https://yourdomain.com
Authorization callback URL http://localhost:9000/api/v1/auth/oauth/github/callback https://api.yourdomain.com/api/v1/auth/oauth/github/callback
Application description (可选) (可选)

注意Callback URL 必须与后端路由完全匹配,包括协议(http/https)。

点击 Register application

1.3 获取凭证

注册完成后,在应用详情页:

  1. 记下 Client ID(形如 Ov23lixxxxxxxxxx
  2. 点击 Generate a new client secret
  3. 记下 Client Secret(只显示一次,务必保存)

如果忘记 Secret,可以重新生成,旧的会立即失效。

2. 配置后端环境变量

编辑项目根目录 .env 文件:

# ── OAuth2 社交登录 ──────────────────────────────────────────
OAUTH_ENABLED=true
OAUTH_REDIRECT_URL=http://localhost:3000/auth/callback

# GitHub OAuth
OAUTH_GITHUB_CLIENT_ID=Ov23liwYwANw8fvQVhI0
OAUTH_GITHUB_CLIENT_SECRET=42ab89916b6731b3ead0dbfadca581aecbb6cb60

配置项说明

环境变量 必填 默认值 说明
OAUTH_ENABLED false 总开关,必须设为 true
OAUTH_REDIRECT_URL http://localhost:3000/auth/callback 登录成功后 302 重定向到前端的地址
OAUTH_GITHUB_CLIENT_ID - GitHub OAuth App 的 Client ID
OAUTH_GITHUB_CLIENT_SECRET - GitHub OAuth App 的 Client Secret

修改 .env 后必须重启后端进程才能生效。

3. 运行数据库迁移

OAuth 功能需要 3 张新表:

表名 用途
oauth_accounts 存储 OAuth 绑定关系
oauth_states 存储 PKCE state(短期,10 分钟过期)
users (改动) 新增 email_verified 字段

迁移文件:migrations/019_oauth.sql

首次启动后端时自动执行,也可以手动运行:

cargo sqlx migrate run --features "db-sqlite"

4. 启动后端

SWAGGER_UI_DOWNLOAD_URL=file:///tmp/swagger-ui.zip cargo run --features "db-sqlite"

启动成功后,日志中应看到:

OAuth provider registered: github

如果没有看到这行,说明 OAUTH_GITHUB_CLIENT_IDOAUTH_GITHUB_CLIENT_SECRET 未正确配置。

5. 验证配置

# 查看已配置的 OAuth Provider
curl http://localhost:9000/api/v1/auth/oauth/providers

# 预期返回:
# {"code":0,"message":"操作成功","data":[{"name":"github","configured":true}]}

6. 测试完整流程

6.1 发起 OAuth 登录

在浏览器中打开:

http://localhost:9000/api/v1/auth/oauth/github

预期行为:302 重定向到 GitHub 授权页面,URL 形如:

https://github.com/login/oauth/authorize
  ?client_id=Ov23liwYwANw8fvQVhI0
  &state=a3f8b2c1d4e5...64字符随机串)
  &code_challenge=E9Melhoa2OwvFrEM...PKCE challenge
  &code_challenge_method=S256
  &scope=user:email

6.2 在 GitHub 授权

点击 Authorize 按钮。

6.3 GitHub 回调

GitHub 将用户重定向到:

http://localhost:9000/api/v1/auth/oauth/github/callback?code=xxx&state=xxx

后端处理流程:

  1. 校验 state(防 CSRF,一次性使用)
  2. code + PKCE code_verifier 换 access_token
  3. 获取 GitHub 用户信息(/user + /user/emails
  4. 查找/创建本地用户
  5. 签发 JWT

6.4 重定向到前端

处理完成后,302 重定向到:

http://localhost:3000/auth/callback
  ?access_token=eyJhbGciOiJIUzI1NiIs...
  &refresh_token=a3f8b2c1d4e5f6...
  &expires_in=900

6.5 curl 测试(不跟随重定向)

# 测试发起授权
curl -v http://localhost:9000/api/v1/auth/oauth/github 2>&1 | grep "Location:"

# 应输出类似:
# Location: https://github.com/login/oauth/authorize?client_id=...

7. 前端对接

7.1 登录页添加 GitHub 按钮

// web/src/app/login/page.tsx
const API_BASE = "http://localhost:9000/api/v1";

<a href={`${API_BASE}/auth/oauth/github`}>
  <GitHubIcon />
  GitHub 登录
</a>

直接用 <a href> 跳转,不需要 fetch。浏览器会跟随 302 重定向到 GitHub。

7.2 创建 OAuth 回调页

// web/src/app/auth/callback/page.tsx
"use client";

import { useEffect } from "react";
import { useRouter } from "next/navigation";

export default function OAuthCallback() {
  const router = useRouter();

  useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    const accessToken = params.get("access_token");
    const refreshToken = params.get("refresh_token");
    const expiresIn = params.get("expires_in");
    const error = params.get("error");

    if (error) {
      console.error("OAuth error:", error);
      router.push("/login?error=oauth_failed");
      return;
    }

    if (accessToken) {
      localStorage.setItem("access_token", accessToken);
      localStorage.setItem("refresh_token", refreshToken || "");
      router.push("/");
    } else {
      router.push("/login?error=no_token");
    }
  }, [router]);

  return <div>正在登录...</div>;
}

7.3 账号设置页 — 绑定/解绑

// 查看已绑定 Provider
const { data } = useQuery({
  queryKey: ["oauth-bindings"],
  queryFn: () =>
    fetch("http://localhost:9000/api/v1/auth/oauth/bindings", {
      headers: { Authorization: `Bearer ${token}` },
    }).then((r) => r.json()),
});

// 解绑
const unbind = async (provider: string) => {
  await fetch(`http://localhost:9000/api/v1/auth/oauth/${provider}/unbind`, {
    method: "DELETE",
    headers: { Authorization: `Bearer ${token}` },
  });
};

8. API 端点完整列表

方法 路径 认证 说明
GET /api/v1/auth/oauth/{provider} 发起 OAuth 登录,302 到 Provider
GET /api/v1/auth/oauth/{provider}/callback Provider 回调处理
GET /api/v1/auth/oauth/providers 已配置的 Provider 列表
GET /api/v1/auth/oauth/bindings 需登录 当前用户绑定列表
DELETE /api/v1/auth/oauth/{provider}/unbind 需登录 解绑指定 Provider

9. 用户场景说明

场景 A:首次 GitHub 登录(自动注册)

1. 用户点击 "GitHub 登录"
2. GitHub 授权
3. 后端发现该 GitHub ID 未绑定任何本地用户
4. 自动创建本地用户:
   - username = GitHub login 名(冲突时追加前缀/随机后缀)
   - email = GitHub 主邮箱
   - password = 占位符("!oauth:github:xxx"
   - role = "reader"
   - avatar = GitHub 头像
   - email_verified = 1
5. 创建 oauth_accounts 绑定
6. 签发 JWT,重定向到前端

场景 B:已有账号,邮箱匹配自动绑定

1. 用户已用 email 注册(如 alice@gmail.com
2. GitHub 账号主邮箱也是 alice@gmail.com
3. 首次用 GitHub 登录
4. 后端发现邮箱匹配已有用户
5. 自动创建 oauth_accounts 绑定
6. 签发 JWT,重定向到前端

场景 C:再次 GitHub 登录(已有绑定)

1. 用户再次点击 "GitHub 登录"
2. GitHub 授权
3. 后端在 oauth_accounts 找到已有绑定
4. 更新 GitHub token/profile 信息
5. 签发 JWT,重定向到前端

场景 D:已登录用户绑定 GitHub

1. 用户在账号设置页点击 "绑定 GitHub"
2. 前端带 token 跳转 /api/v1/auth/oauth/github
   (后端从 JWT 提取 user_id,存入 oauth_states
3. GitHub 授权
4. 回调时后端发现 state.user_id 存在
5. 创建 oauth_accounts 绑定到该用户
6. 签发 JWT,重定向到前端

场景 E:解绑

1. 用户在账号设置页点击 "解绑 GitHub"
2. DELETE /api/v1/auth/oauth/github/unbind
3. 后端检查安全性:
   - 如果用户没有密码(纯 OAuth 用户)且只剩这一个绑定 → 拒绝
   - 否则 → 删除绑定

10. 安全机制

PKCEProof Key for Code Exchange

每次授权请求:

1. 生成 code_verifier43 字符随机串)
2. code_challenge = BASE64URL(SHA256(code_verifier))
3. 授权 URL 携带 code_challenge + method=S256
4. 回调时发送 code_verifier
5. GitHub 用 SHA256 验证匹配

防止 authorization code 被截获后滥用。

State 参数(CSRF 防护)

1. 生成 state64 字符随机 hex
2. 存入 oauth_states 表(10 分钟过期)
3. 授权 URL 携带 state
4. 回调时校验 state 匹配
5. 用后即删(一次性)

解绑安全

无密码的纯 OAuth 用户仅剩一个绑定时,拒绝解绑,防止用户无法登录。

11. 生产环境部署

必须修改的配置

# .env.production
OAUTH_ENABLED=true
OAUTH_REDIRECT_URL=https://yourdomain.com/auth/callback

# 强密钥
JWT_SECRET=至少32字符的随机字符串

# CORS
CORS_ORIGINS=https://yourdomain.com

GitHub OAuth App 配置更新

在 GitHub OAuth App 设置中更新:

字段
Homepage URL https://yourdomain.com
Authorization callback URL https://api.yourdomain.com/api/v1/auth/oauth/github/callback

如果前后端同域(通过 nginx 反向代理),callback URL 可以是: https://yourdomain.com/api/v1/auth/oauth/github/callback

Nginx 配置示例

server {
    listen 443 ssl;
    server_name yourdomain.com;

    # 前端
    location / {
        proxy_pass http://127.0.0.1:3000;
    }

    # 后端 API
    location /api/ {
        proxy_pass http://127.0.0.1:9000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

12. 故障排查

"OAuth is not enabled"

检查:
1. .env 中 OAUTH_ENABLED=true(无 # 前缀)
2. 重启了后端进程
3. 进程启动日志中有 "OAuth provider registered: github"

"OAuth provider 'github' is not configured"

检查:
1. OAUTH_GITHUB_CLIENT_ID 和 OAUTH_GITHUB_CLIENT_SECRET 都已设置
2. 值不为空
3. 重启了后端进程

GitHub 回调报 400 "invalid or expired OAuth state"

可能原因:
1. state 超过 10 分钟过期(用户在 GitHub 页面停留太久)
2. state 被重复使用(浏览器刷新了回调 URL
3. oauth_states 表未创建(迁移未执行)

解决:重新发起授权(重新访问 /api/v1/auth/oauth/github

GitHub 回调报 "GitHub token exchange failed"

可能原因:
1. Client Secret 不正确
2. Authorization code 已过期(10 分钟有效期)
3. GitHub OAuth App 的 callback URL 不匹配

解决:
1. 在 GitHub App 设置页重新生成 Secret
2. 确认 callback URL 完全匹配

邮箱未自动获取

GitHub API 只返回 public 邮箱。私有邮箱需要:
1. 授权时 scope=user:email(已包含)
2. 额外调用 GET /user/emails API(已自动处理)

如果仍然没有邮箱,检查:
1. GitHub 账号是否设置了主邮箱
2. 邮箱是否已验证

重定向到前端后 token 无效

检查:
1. OAUTH_REDIRECT_URL 是否正确
2. JWT_SECRET 是否在重启后变了
3. 前端是否正确提取 URL 参数