CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
OAuth 2.0 与 OpenID Connect/课时/第三方登录
RawBlame
课时目录12 节

OAuth 2.0 与 OpenID Connect

  1. 01OAuth 是什么
  2. 02授权码流
  3. 03PKCE 与 SPA
  4. 04OpenID Connect
  5. 05Scope 与权限
  6. 06Refresh Token
  7. 07Spring OAuth2 Client
  8. 08Resource Server
  9. 09第三方登录
  10. 10OAuth vs 本站 JWT 登录
  11. 11常见安全问题
  12. 12综合练习

第三方登录

GitHub/Google 登录流程直觉。

30 分钟

第三方登录

这一课你将能做到

  1. 从注册 GitHub OAuth App 到跑通回调,说清「没有 id_token 的平台如何用 /user 接口充当身份源」
  2. 对比 GitHub OAuth(API 授权型)与 Google OIDC(身份认证型)的差异,为不同平台选对主键与校验方式
  3. 设计产品规则:首次登录建号或绑定已有账号、解绑流程、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 /userid_token(JWT,可验签)
稳定主键数字 idclaim sub
scoperead:user user:emailopenid 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 账号永远保留密码登录;审计日志记录绑定/解绑事件

讲清楚要点

  1. 社交登录是产品包装,协议内核仍是授权码流(+机密 Client 或 PKCE)——不要发明「密码代填」之类的私有实现。
  2. GitHub 与 Google 的身份断言形态不同,适配层统一成 provider + externalId 再入库;上层代码永远不感知平台差异。
  3. 邮箱可变、可未验证、可能为空:绝不用 email 当唯一键;要按 email 合并账号,必须在「已登录 + 邮箱已验证」的前提下走显式绑定流程。
  4. secret 与换票必须在后端:platform 只负责跳转与接收本站 JWT(第 7 课的成功处理器模式)。
  5. 账号劫持影响全站:绑定/解绑必须在登录态下操作,异常登录要有通知,解绑留审计日志。
  6. 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 拿不到 tokencode 交给后端约定路径

卡点急救

  1. 现象:GitHub 回调 404。Callback URL 与 Spring 路径不一致。逐字核对 http://localhost:8080/login/oauth2/code/github——registrationId 是 github(对应 yaml 里的 key),不是 Client ID。
  2. 现象:首次登录建了号,第二次登录又建了一个。binding 表唯一键没建,或取 externalId 的字段不稳定。确认 UNIQUE KEY uk_provider_subject 存在、代码读的是 id/sub 而不是 login。
  3. 现象: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,才是可控方案。

自测三问

  1. GitHub 与 Google 各自的「稳定用户 id」从哪来?校验方式差在哪?
  2. 画出从「GitHub 登录」按钮到 platform cookie 的完整跳转链。
  3. 首次 OAuth 登录「自动建号」和「必须绑定」各有什么风险?你选哪个、为什么?

小结一句话

社交登录 = 标准 OAuth/OIDC + provider/externalId 适配层 + 本站 JWT 会话;密钥留后端,建号/绑定/解绑规则想清楚再上线。

延伸

  • GitHub:Authorizing OAuth apps
  • Google Identity:OpenID Connect
  • 下一课:OAuth vs 本站 JWT 登录——把两条轨放上天平,讲透何时该上 OAuth、JWT 无状态的利弊与 refresh 轮换。
加载本节练习…
← 上一课Resource Server下一课 →OAuth vs 本站 JWT 登录