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

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

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

学习

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

更多

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

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
OAuth 2.0 与 OpenID Connect/课时/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综合练习

OpenID Connect

id_token、UserInfo、与 OAuth2 关系。

35 分钟

OpenID Connect

这一课你将能做到

  1. 说清 OIDC 与 OAuth 2.0 的关系(OIDC = OAuth 2.0 + 标准化身份层),区分 access_token 与 id_token 的受众和用途
  2. 解码 id_token 的 payload,列出并执行五项必检:签名、iss、aud、exp、nonce
  3. 实现联邦登录的落库与发本站 JWT:验 id_token → 用 (iss, sub) 查找或创建本地用户 → backend 签发站内 JWT

为什么要学

OAuth 2.0 回答的是:「这个 App 被允许访问哪些资源?」但产品要的「用 Google 登录」还要回答另一个问题:「眼前这个用户是谁?邮箱是多少?要不要给他建个号?」

纯 OAuth 没有「登录成功」的标准表达:你可以拿 access_token 去调用户信息接口,但每个平台返回的字段、格式、含义都不一样(GitHub 返回 id,微信返回 openid,新浪返回 uid……),每接一个平台就要写一套适配。

OIDC(OpenID Connect)在 OAuth 2.0 之上定义了标准身份层:

  • scope 里带 openid,token 端点就多返回一份 id_token(标准 JWT);
  • id_token 里的 claims(sub 用户唯一标识、email、name…)格式统一;
  • 另有标准 UserInfo 端点、Discovery 元数据(/.well-known/openid-configuration)。

Google、Auth0、Keycloak、企业 SSO 全都按 OIDC 来。面试必考:「OIDC 和 OAuth 什么关系?」——背:OIDC = OAuth 2.0 + 身份层;OAuth 管授权(能访问什么),OIDC 管认证(你是谁);OIDC 完全构建在 OAuth 流程之上,id_token 是它最核心的新增产物。

先建立图像

纯 OAuth(授权 API):
  用户同意 → access_token → Client 调 Resource Server 的 API
  「这个 App 可以读我的资料」——但「资料主人」没有标准身份格式

OIDC(认证 + 可选授权):
  scope=openid profile email
  用户同意 → access_token + id_token
                │            │
                │            └─► Client 验签后相信:sub=110169... 就是登录者
                │                (id_token 是「给 Client 看的身份证」)
                └─► Client 用 access_token 调 API / UserInfo

三种 token 各归各位:

产物给谁看典型格式用途
access_tokenResource Server不透明串或 JWT开门调 API
id_tokenClient 自己一定是 JWT证明「登录的是谁」
refresh_tokenClient(妥善保管)通常不透明换新 access_token

比喻:access_token 是房卡(给门锁看);id_token 是前台给你的「已核实身份回执」(给你自己看,门锁不认它)。

id_token 的 payload 长这样:

{
  "iss": "https://accounts.google.com",
  "sub": "110169484474386276334",
  "aud": "your-client-id",
  "exp": 1735689600,
  "iat": 1735686000,
  "nonce": "a1b2c3d4",
  "email": "alice@example.com",
  "email_verified": true,
  "name": "Alice Wang",
  "picture": "https://lh3.googleusercontent.com/..."
}

跟着做

步骤 1:authorize 时带上 openid

GET https://accounts.google.com/o/oauth2/v2/auth
  ?response_type=code
  &client_id=你的ClientID
  &redirect_uri=http://localhost:8080/login/oauth2/code/google
  &scope=openid%20email%20profile
  &state=随机串
  &nonce=加密随机串(防重放)

记住一条规则:scope 里没有 openid,就没有标准 id_token——这是 OIDC 的总开关。

步骤 2:亲手解码一份 id_token

token 端点响应里的 id_token 是三段式 header.payload.signature。把 payload(第二段)做 Base64URL 解码:

// 中文注释:仅本地解码查看字段;生产校验必须验签,绝不能只解码
const payload = JSON.parse(
  Buffer.from(idToken.split(".")[1], "base64url").toString()
);
console.log(payload);

对照上面的 JSON,找出 iss、sub、aud、exp、nonce 五个字段。

步骤 3:写出 Java 校验(骨架完整可落地)

// 中文注释:id_token 校验五件事——验签、iss、aud、exp、nonce;任何一项失败按未登录处理
// 实际项目推荐用 nimbus-jose-jwt 或 Spring Security 的 JwtDecoder(内置 JWKS 验签)
public void validateIdTokenClaims(Jwt idToken, String expectedNonce) {
    Instant now = Instant.now();
    if (idToken.getExpiresAt() == null || idToken.getExpiresAt().isBefore(now)) {
        throw new BadJwtException("id_token 已过期");
    }
    if (!"https://accounts.google.com".equals(idToken.getIssuer().toString())) {
        throw new BadJwtException("iss 不是预期的 IdP");
    }
    if (!idToken.getAudience().contains(clientId)) {
        throw new BadJwtException("aud 不是发给我这个 Client 的");
    }
    if (expectedNonce != null
            && !expectedNonce.equals(idToken.getClaimAsString("nonce"))) {
        throw new BadJwtException("nonce 不匹配,疑似重放");
    }
}

五项必检各自防什么:

检查项防的攻击
验签(JWKS 公钥)伪造 token——任何人都能 base64 编一段假 payload
iss拿「别的 IdP 签发的合法 token」来冒充
aud拿「发给别的应用」的 token 来你这里充数
exp重放过期旧 token
nonce把某个会话的登录凭证重放进另一个会话

步骤 4:可选的 UserInfo 端点

# 中文注释:某些 claim(如 email)可能只在 UserInfo 返回;用 access_token 调用
curl -i https://openidconnect.googleapis.com/v1/userinfo \
  -H "Authorization: Bearer ACCESS_TOKEN"

步骤 5:映射到本站用户并发本站 JWT

// 中文注释:联邦登录三步——找/建用户 → 发本站 JWT → 跳回前端
User user = userService.findByOidc(issuer, sub)
        .orElseGet(() -> userService.createOidcUser(issuer, sub, email, name));

String siteToken = jwtUtil.sign(user.getId(), user.getRole()); // 本站自研 JWT
String redirect = frontendUrl + "/login/callback?token="
        + URLEncoder.encode(siteToken, StandardCharsets.UTF_8);
response.sendRedirect(redirect);

本地用户绑定的关键表设计:

-- 中文注释:provider + external_id 联合唯一;email 只当资料不当主键
ALTER TABLE user_oauth_binding
  ADD UNIQUE KEY uk_provider_subject (provider, external_id);

步骤 6:对比 GitHub(没有 OIDC 的平台)

GitHub:access_token → GET api.github.com/user → 拿数字 id 当 sub 用
Google:id_token 直接读 sub(可验签,不必再调接口)
统一适配层:provider + externalId 两字段把不同平台抹平

讲清楚要点

  1. OAuth 不定义身份,OIDC 补上:openid scope 是开关,id_token 是产物,UserInfo 是补充通道。
  2. id_token 的受众是 Client,不是 Resource Server:不要拿 id_token 去 API 当 Bearer;API 只认 access_token(或本站自己的凭证)。
  3. 五项校验缺一不可:验签、iss、aud、exp、nonce。只解码不验签等于没校验——payload 谁都能编。
  4. sub 是同一位 IdP 下用户的稳定主键:本地表用 (provider, external_id) 联合唯一;email 可能变更、可能未验证,不能当主键。
  5. 登录成功 ≠ 拿到 IdP 的 token 就完事:标准做法是映射本地用户、发本站会话凭证(本站 JWT),站内权限与撤销不依赖 IdP。
  6. Discovery 简化接入:从 https://IdP域名/.well-known/openid-configuration 获取 authorize/token/jwks 端点,别硬编码。

你可能会懵的地方

  • 既然拿到了 access_token,为什么还要 id_token? access_token 可能是不透明串,Client 解析不了「是谁」;就算能解析,语义也是资源服务器说了算。id_token 是专门给 Client 的标准化身份断言,格式固定、可验签。

  • id_token 可以当 Bearer 调自己的 API 吗? 不建议。aud 语义就错了(发给 Client 不是发给 API),安全模型会混乱。本站 API 继续用本站 JWT;调 IdP 的 API 用 IdP 的 access_token。

  • 时钟偏差会不会把好 token 判成过期? 会有几十秒级别的偏差问题。校验库一般允许 30-60 秒 clock skew,服务器保持 NTP 对时。

  • 一个用户有 GitHub 和 Google 两个账号,怎么合成一个本地用户? 产品决策:常见做法是登录后提供「绑定其他账号」入口(已登录状态下绑定并验证邮箱),而不是首次登录时按 email 静默合并——email 验证状态不可信时,静默合并会被用来接管账号。

  • nonce 到底防什么? 防重放:攻击者诱导你的应用用「他准备好的 code/token」完成登录(login CSRF 的变体)。authorize 时生成随机 nonce 存会话,id_token 里的 nonce 必须与会话里那份一致,确保这份 id_token 是「本次登录流程」的产物。

错法对照

错法问题正解
base64 解码 id_token 后直接用,不验签伪造 token 畅通无阻JWKS 验签 + iss/aud/exp/nonce 五检
用 email 当本地用户唯一键email 可改、可未验证、可被他人注册(provider, external_id) 唯一,email 只作资料
把 id_token 塞进 Authorization 头调业务 APIaud 语义错误,安全模型混乱业务 API 用本站 JWT;IdP API 用其 access_token
以为「配了 OAuth 就自动是 OIDC」少了 openid scope,拿不到标准 id_token检查 scope 与 id_token 是否真的返回
忘了生成/校验 nonce回调可被重放进别的会话authorize 时生成随机 nonce,回调严格比对

卡点急救

  1. 现象:token 端点正常返回,但代码里拿不到 id_token。检查 authorize 的 scope 是否包含 openid(漏了就没有);确认响应 JSON 字段名是 id_token;GitHub 这类非 OIDC 平台本来就没有,别按 OIDC 解析。
  2. 现象:验签失败 / signature validation failed。JWKS 地址是否正确(先 curl /.well-known/openid-configuration);token 的 kid 能否在 JWKS 里找到(IdP 轮换密钥后旧缓存要刷新);本地时间是否准确。
  3. 现象:同一用户每次登录都新建本地用户。查 (provider, external_id) 唯一键是否建了;取 external_id 的字段是否稳定(GitHub 用数字 id,不要用会改名的 login;Google 用 sub)。

面试常问

  • OIDC 和 OAuth 2.0 是什么关系? OIDC 是构建在 OAuth 2.0 之上的身份认证层。OAuth 管授权(App 能访问什么资源),OIDC 管认证(用户是谁)。技术上:scope 带 openid → token 响应多出 id_token(JWT);另有 UserInfo 端点与 Discovery。一句话:OAuth 问「能做什么」,OIDC 问「你是谁」。

  • id_token 和 access_token 的区别? 受众不同:access_token 给资源服务器验(开门),id_token 给客户端验(认人)。格式上 id_token 必为 JWT,access_token 可以是 JWT 也可以是不透明串。绝不能混用——拿 id_token 调 API、拿 access_token 解析身份都是错误姿势。

  • 第三方登录后为什么要发本站自己的 JWT,而不是直接用 IdP 的 token? 三点:站内权限模型(角色、会员状态)在本地库,IdP 不知道;撤销可控——本站 JWT 短寿命 + 会话版本,不依赖 IdP 可用性;解耦——换 IdP 或 IdP 故障时站内会话不受牵连。

自测三问

  1. id_token 的五项必检是什么?各防什么攻击?
  2. access_token、id_token、refresh_token 分别给谁用?
  3. 本地用户表为什么用 (provider, external_id) 而不是 email 做唯一键?

小结一句话

OIDC = OAuth 2.0 + 标准身份层:openid scope 开关、id_token 身份断言、UserInfo 补充;Client 验 id_token 认人,API 验 access_token 开门。

延伸

  • OpenID Connect Core 1.0
  • openid.net:What is OpenID Connect?
  • 下一课:Scope 与权限——「同意页上勾的那些框」如何变成 API 里可执行的授权规则。
加载本节练习…
← 上一课PKCE 与 SPA下一课 →Scope 与权限