2. OIDC 核心概念:ID 、Access 、UserInfo 端点、Claims

好,我们直接进入正题。上一章我们聊了为什么需要 OIDC 以及它解决了什么问题。这一章,我们得把这套体系里的几个核心零件拆开来看——ID 、Access 、UserInfo 端点 和 Claims。这四个东西你搞明白了,OIDC 的骨架基本就搭起来了。

2.1 ID :你到底是谁?

ID 是 OIDC 里最重要的安全令牌。说白了,它是一个 JWT(JSON Web ),由授权服务器签发,客户端拿到来验证用户的身份。你想想看,传统方式下你是谁?靠 session cookie 里的 session ID。但 OIDC 不一样,它直接把用户信息编码进一个令牌里,客户端验签通过就知道用户是谁。我记得我第一次接触 OIDC 时,觉得这设计太聪明了。

核心要点:ID 是一个签名后的 JWT,必须包含 iss(签发者)、sub(用户唯一标识)、aud(受众)、exp(过期时间)和 iat(签发时间)等标准 Claims。

ID 的结构长什么样?

让我给你拆解一下。一个典型的 ID 包含三部分,用点号分隔:Header、Payload、Signature。

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiw
iaXNzIjoiaHR0cHM6Ly9hdXRoLmV4YW1wbGUuY29tIn0.
nK3XFSbC6X6Q5PcAq6yZ8s9Q4bRc9k1X2w3v4b5y6A7

Header 部分声明了签名算法,比如 RS256。Payload 部分才是真正承载用户信息的 Claims。Signature 则用来保证整个令牌没有被篡改。

ID 中必须包含的 Claims

在我项目中遇到过的一个坑是,有些初学者实现 OIDC 客户端时,忘了验证 aud(受众)声明,结果被重放攻击打了个措手不及。这里我列一个标准 Claims 表,你对照着看:

Claim 含义 必须 备注
iss 签发者 是 必须是期望的授权服务器 URL
sub 用户唯一标识 是 在同一签发者下是唯一的
aud 受众 是 必须包含你的 client_id
exp 过期时间 是 必须在当前时间之后
iat 签发时间 是 不可篡改

我的建议:在生产环境中,ID 的过期时间通常设为 5 到 10 分钟。别设太长——你想想看,如果泄露了,攻击者就能在窗口期内冒充用户。

2.2 Access :你能做什么?

如果说 ID 回答的是“你是谁”,那 Access 回答的就是“你能做什么”。Access 的格式 不一定是 JWT——这又是一个常见的误解。它可以是任意格式的令牌,在 OAuth 2.0 体系里定义的,用来访问受保护资源。

我个人习惯把 Access 看作是 API 的通行证。你拿着它,UserInfo 端点才会给你身份信息,后端 API 才会允许你读取或写入数据。

ID vs Access 的对比

很多新手容易搞混这两个令牌。我在这里用个表格帮你理清:

属性 ID Access
目的 认证(你是谁?) 授权(你能做什么?)
格式 通常是 JWT 可以是 JWT,也可以是不透明令牌
过期时间 短(5-10 分钟) 相对长(15 分钟到几小时)
客户端使用方式 直接展示给用户?不 放在 HTTP Authorization Header 里
是否可被用户看到 理论上可以(JWT 是不加密的) 通常不,特别是对不透明令牌

我曾经踩过的坑:有一次我在前端把 Access 直接暴露在 URL 参数里。结果不小心被第三方脚本抓到了,对方直接拿着这个令牌把用户的数据拉走。记住:不要把 Access 放在 URL 查询参数里,除非你能确保传输是 HTTPS 且令牌立即失效。

2.3 UserInfo 端点:获取用户的详细信息

ID 包含了用户的基本 Claims,但你想想看,如果用户有 50 个属性(比如头像 URL、手机号码、地址等),ID 会变得巨大,传输和验证都会变慢。这时候就需要 UserInfo 端点 了。

UserInfo 端点是一个受保护的 API,客户端拿着 Access 去调用,就能获取到用户的完整 Claims 集合。它是 OIDC 标准的组成部分,是一个 OAuth 2.0 Bearer 保护的资源服务器。

调用 UserInfo 端点的典型流程

  1. 客户端使用 Authorization Code 流程拿到 Access 。
  2. 客户端把 Access 放在 HTTP Authorization: Bearer <> 头中。
  3. 客户端发送 GET 请求到 https://auth.example.com/userinfo。
  4. 授权服务器验证 Access 是否有效。
  5. 如果有效,返回 JSON 格式的 UserInfo Claims。

嗯,这里要注意:UserInfo 端点返回的 Claims 必须与 ID 中的 sub 一致。换句话说,无论是 ID 还是 UserInfo 端点,同一用户的 sub 必须完全一样。这个一致性是 OIDC 的核心要求。

2.4 Claims:用户的属性数据

Claims 其实就是 OIDC 里对用户属性的称呼。每个 Claim 就是一个键值对,比如 name: "张三" 就是一个 Claim。

OIDC 定义了一组标准的 Claims,但同时也允许你自定义 Claims。标准 Claims 包括:

  • sub — 用户唯一标识
  • name — 用户全名
  • given_name — 名
  • family_name — 姓
  • email — 邮箱地址
  • email_verified — 邮箱是否已验证
  • picture — 头像 URL
  • phone_number — 手机号

我建议你在从 ID 里获取 Claims 时,不要完全信任这些字段——因为 ID 是不加密的,客户端可以读但不能改。而 UserInfo 端点的返回结果,才是你应该信赖的用户数据。

2.5 一张图看懂 OIDC 核心概念的关系

说了这么多,你可能有点晕。没关系,我画了一张结构图帮你把这些概念串起来:

OIDC 核心概念结构图 客户端 (Web App / Mobile App) 授权服务器 (Issuer) UserInfo 端点 /userinfo ID (JWT,含用户身份) Access (Bearer ) Claims (sub, name, email...) 认证请求 签发 ID + Access 携带 Access 请求 返回 🔑 核心流程总结: 用户认证后,授权服务器返回 ID (用户身份)和 Access (访问权限)。 客户端用 Access 调用 UserInfo 端点,获取完整的 Claims。

这张图其实已经帮你理清了整套流程。用户第一次访问你的应用时,客户端向授权服务器发起认证请求;授权服务器校验通过后,返回一个 ID 和一个 Access ;客户端需要进一步获取用户详细信息时,就拿 Access 去请求 UserInfo 端点;UserInfo 端点返回用户 Claims——这些 Claims 就是你的应用最终用到的用户属性。

2.6 实际场景中的常见问题

我曾经遇到过一次严重的 bug:公司做 SSO 合并,两个系统用了不同的授权服务器。用户登录系统 A 后,Access 被拿去调用系统 B 的 UserInfo 端点——结果当然失败了。为什么呢?因为 Access 是发给系统 A 的,受众(aud)不对。记住:不同的授权服务器签发的令牌,不能混用。

还有个小技巧:在生产环境中,我建议你始终将 ID 和 Access 的验证逻辑写在一起。比如客户端收到这两个令牌,先验证 ID 的签名和标准 Claims,再拿着 Access 去调用 UserInfo 端点。这样能保证用户的身份和授权状态都是一致的。