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 端点的典型流程
- 客户端使用 Authorization Code 流程拿到 Access 。
- 客户端把 Access 放在 HTTP
Authorization: Bearer <>头中。 - 客户端发送 GET 请求到
https://auth.example.com/userinfo。 - 授权服务器验证 Access 是否有效。
- 如果有效,返回 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— 头像 URLphone_number— 手机号
我建议你在从 ID 里获取 Claims 时,不要完全信任这些字段——因为 ID 是不加密的,客户端可以读但不能改。而 UserInfo 端点的返回结果,才是你应该信赖的用户数据。
2.5 一张图看懂 OIDC 核心概念的关系
说了这么多,你可能有点晕。没关系,我画了一张结构图帮你把这些概念串起来:
这张图其实已经帮你理清了整套流程。用户第一次访问你的应用时,客户端向授权服务器发起认证请求;授权服务器校验通过后,返回一个 ID 和一个 Access ;客户端需要进一步获取用户详细信息时,就拿 Access 去请求 UserInfo 端点;UserInfo 端点返回用户 Claims——这些 Claims 就是你的应用最终用到的用户属性。
2.6 实际场景中的常见问题
我曾经遇到过一次严重的 bug:公司做 SSO 合并,两个系统用了不同的授权服务器。用户登录系统 A 后,Access 被拿去调用系统 B 的 UserInfo 端点——结果当然失败了。为什么呢?因为 Access 是发给系统 A 的,受众(aud)不对。记住:不同的授权服务器签发的令牌,不能混用。
还有个小技巧:在生产环境中,我建议你始终将 ID 和 Access 的验证逻辑写在一起。比如客户端收到这两个令牌,先验证 ID 的签名和标准 Claims,再拿着 Access 去调用 UserInfo 端点。这样能保证用户的身份和授权状态都是一致的。