第三方登录
GitHub/Google 登录流程直觉。
第三方登录
这一课你将能做到
- 从注册 GitHub OAuth App 到跑通回调,说清「没有 id_token 的平台如何用
/user 接口充当身份源」
- 对比 GitHub OAuth(API 授权型)与 Google OIDC(身份认证型)的差异,为不同平台选对主键与校验方式
- 设计产品规则:首次登录建号或绑定已有账号、解绑流程、admin 保留密码回退,用统一的 provider + externalId 适配层抹平平台差异
为什么要学
「用 GitHub / Google 登录」是用户感知最强的 OAuth/OIDC 落地:你不碰 IdP 的密码,却能建立本地账号。协议骨架在第 2-4 课已经搭好,这一课的难点在「平台的差异」与「产品的规则」:
- 平台差异:GitHub 没有标准 id_token,身份靠调 API 拿;Google 是标准 OIDC,身份直接写在 id_token 里。主键、校验、资料获取方式都不同。
- 产品规则:首次 OAuth 登录是自动建号,还是要求绑定已有邮箱账号?解绑后用什么登录?admin 能不能只留社交登录?
这些决策做错,轻则体验割裂,重则账号被接管。全栈工程师的价值就在于把这些缝缝补补收进一个干净的适配层。
先建立图像
┌────────────┐ ①跳转授权 ┌──────────────┐ ③code换token ┌────────────┐
│ platform │ ──────────► │ Spring 后端 │ ─────────────► │ IdP │
│ 「GitHub │ │ (OAuth Client)│ ◄── token ──── │ GitHub / │
│ 登录」按钮 │ ◄───────── │ success: │ ④调API/id_token│ Google │
└────────────┘ ⑤本站JWT │ findOrCreate │ └────────────┘
写cookie 302回前端 │ → 签发本站JWT │
└──────────────┘
本地表:user_oauth_binding(provider, external_id) 联合唯一
两类 IdP 的关键差异:
| 维度 | GitHub(OAuth 型) | Google(OIDC 型) |
|---|
| 身份证明 | access_token → 调 GET /user | id_token(JWT,可验签) |
| 稳定主键 | 数字 id | claim sub |
| scope | read:user user:email | openid email profile |
| 额外校验 | 无标准声明可验,信任 API 返回 | iss/aud/exp/nonce 五检 |
| 接入姿势 | Spring 内置 CommonOAuth2Provider | 同样内置,配置 registration 即可 |
跟着做
步骤 1:注册 GitHub OAuth App
GitHub → Settings → Developer settings → OAuth Apps → New OAuth App
Application name: CodeRoadMap(本地开发)
Homepage URL: http://localhost:3000
Authorization callback URL:
http://localhost:8080/login/oauth2/code/github
拿到 Client ID / Client Secret 后写入环境变量 GITHUB_CLIENT_ID、GITHUB_CLIENT_SECRET。
步骤 2:Spring 配置承接(复用第 7 课骨架)
spring:
security:
oauth2:
client:
registration:
github:
client-id: ${GITHUB_CLIENT_ID}
client-secret: ${GITHUB_CLIENT_SECRET}
scope: read:user,user:email
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope: openid,profile,email
Google 需要先在 Google Cloud Console 创建 OAuth 客户端,callback 填 http://localhost:8080/login/oauth2/code/google。两个平台跑的是同一条授权码流,只是 registration 各一份。
步骤 3:看 GitHub 如何「无 id_token 取身份」
# 中文注释:GitHub 的身份来自 API;记下稳定字段 id(数字),login(用户名)可改
curl -i https://api.github.com/user \
-H "Authorization: Bearer GITHUB_ACCESS_TOKEN" \
-H "Accept: application/vnd.github+json"
返回 JSON 里关注:id(稳定主键)、login、avatar_url、email(可能为 null)。
步骤 4:Google OIDC 对照
scope=openid email profile
→ token 响应含 id_token
→ 验签(JWKS) + iss/aud/exp/nonce 五检
→ 读 sub 作为外部主键
→ 不必「猜」唯一键,也不必再调接口(资料都在 claims 或 UserInfo 里)
步骤 5:写统一适配层(核心代码)
// 中文注释:把「平台差异」封在 findOrCreate 里,上层只认 provider + externalId
@Service
public class OAuthUserService {
public User findOrCreateFromOAuth(String provider, String externalId,
Map<String, Object> attrs) {
return bindingRepo.findByProviderAndExternalId(provider, externalId)
.map(binding -> userRepo.findById(binding.getUserId()).orElseThrow())
.orElseGet(() -> {
// 首次登录:按产品规则建号(此处选择自动建号,默认最低权限 USER)
User user = new User();
user.setNickname((String) attrs.getOrDefault("name", externalId));
user.setAvatar((String) attrs.get("picture"));
user.setRole("USER");
userRepo.save(user);
OAuthBinding binding = new OAuthBinding(provider, externalId, user.getId());
bindingRepo.save(binding); // uk_provider_subject 唯一键兜底
return user;
});
}
}
-- 中文注释:绑定表结构与唯一约束;email 不当主键
CREATE TABLE user_oauth_binding (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
provider VARCHAR(32) NOT NULL,
external_id VARCHAR(128) NOT NULL,
user_id BIGINT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_provider_subject (provider, external_id)
);
步骤 6:绑定/解绑与产品规则
绑定:已登录状态下,跳转 /oauth2/authorization/github
成功处理器检测到已有本站会话 → 走「绑定分支」(写 binding 表)而非建新号
解绑:本站设置页操作;解绑前校验「至少留一种登录方式」(密码或另一平台)
回退:admin 账号永远保留密码登录;审计日志记录绑定/解绑事件
讲清楚要点
- 社交登录是产品包装,协议内核仍是授权码流(+机密 Client 或 PKCE)——不要发明「密码代填」之类的私有实现。
- GitHub 与 Google 的身份断言形态不同,适配层统一成
provider + externalId 再入库;上层代码永远不感知平台差异。
- 邮箱可变、可未验证、可能为空:绝不用 email 当唯一键;要按 email 合并账号,必须在「已登录 + 邮箱已验证」的前提下走显式绑定流程。
- secret 与换票必须在后端:platform 只负责跳转与接收本站 JWT(第 7 课的成功处理器模式)。
- 账号劫持影响全站:绑定/解绑必须在登录态下操作,异常登录要有通知,解绑留审计日志。
- scope 最小化 + 可撤销:只申请用到的资料权限;在产品里提供「撤销第三方授权」的引导链接(如 GitHub Settings → Applications)。
你可能会懵的地方
-
GitHub 文档为什么不叫 OpenID Connect?
它主要提供 OAuth 访问 API;「登录」功能是你拿 access_token 调 /user 自己拼出来的,语义与 OIDC 的 id_token 不同一套。功能上等效,协议上别混。
-
两个 GitHub 账号能绑同一个本地用户吗?
技术上可以——binding 表允许多行指向同一 user_id。要不要允许是产品决策:多数站允许「一账号多平台登录」,但限制「一平台多账号」。
-
localhost 回调在公司代理/远程开发下失败?
IdP 会对 callback 域名严格匹配。用固定 hosts 域名、SSH 隧道或内网穿透,并把实际浏览器地址登记进 OAuth App;本地与生产分两个 App 最稳。
-
拿了 user:email 仍然没有 email?
主邮箱未公开,需要额外调 GET /user/emails;也可能用户根本没有公开邮箱。主键用 id,email 缺失时引导用户在站内补全。
-
社交登录的用户能直接给 ADMIN 吗?
不能。联邦账号默认最低权限(USER),升 admin 必须走站内授权流程(管理员指派),防止「外部平台账号 = 站内特权」的越权链路。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 前端直连 GitHub 换 token 并藏 secret | 机密进浏览器,等于公开 | Spring Client / BFF 统一换票 |
用 login(用户名)当主键 | 用户改名后账号对不上 | 用数字 id / OIDC sub |
| 按 email 静默合并账号 | 邮箱可被冒用,形成接管链路 | 显式绑定 + 已验证邮箱前提 |
| 社交登录成功直接赋 ADMIN | 外部账号越权站内 | 默认 USER,管理员另行指派 |
| callback 指到前端却期望后端换票 | code 拿不到 token | code 交给后端约定路径 |
卡点急救
- 现象:GitHub 回调 404。Callback URL 与 Spring 路径不一致。逐字核对
http://localhost:8080/login/oauth2/code/github——registrationId 是 github(对应 yaml 里的 key),不是 Client ID。
- 现象:首次登录建了号,第二次登录又建了一个。binding 表唯一键没建,或取 externalId 的字段不稳定。确认
UNIQUE KEY uk_provider_subject 存在、代码读的是 id/sub 而不是 login。
- 现象:Google 登录报
redirect_uri_mismatch 或 400。Google Cloud Console 里登记的 URI 与实际发起的(含协议、端口、路径)必须完全一致;本地开发注意 http://localhost:8080 是否已加入「已获授权的重定向 URI」。
面试常问
-
GitHub 与 Google 获取「稳定用户 id」的方式有什么不同?
Google 是标准 OIDC:id_token 里的 sub,可验签、无需额外请求;GitHub 没有 id_token:用 access_token 调 /user 拿数字 id(不要用可改名的 login)。适配层统一成 provider + externalId 落库。
-
第三方登录的「账号合并」怎么做才安全?
原则:不静默合并。首次登录建新号或要求绑定已有账号(需输入本站密码确认身份);后续合并走「已登录 + 验证另一平台归属」的显式流程;全程审计日志。核心是保证「发起合并的人确实拥有两边的账号」。
-
为什么管理后台不建议「仅社交登录」?
单点依赖外部 IdP 的可用性与策略(封号、风控误伤都会导致管理员进不了后台);密码登录(或 SSO+本地回退)作为应急通道,配合强口令与 2FA,才是可控方案。
自测三问
- GitHub 与 Google 各自的「稳定用户 id」从哪来?校验方式差在哪?
- 画出从「GitHub 登录」按钮到 platform cookie 的完整跳转链。
- 首次 OAuth 登录「自动建号」和「必须绑定」各有什么风险?你选哪个、为什么?
小结一句话
社交登录 = 标准 OAuth/OIDC + provider/externalId 适配层 + 本站 JWT 会话;密钥留后端,建号/绑定/解绑规则想清楚再上线。
延伸