KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
01 · 断言与令牌 — keel 龙骨
类型:衍生概念(服务于「IdP 与 SP」) · 应用课对应:03 章 · 令牌与校验
类型:衍生概念(服务于「IdP 与 SP」) · 应用课对应:03 章 · 令牌与校验
这一页只回答四件事:它为什么会出现、它是什么、它长什么样、它怎么工作。它不讲前端怎么存令牌。
一、它为什么会出现
IdP 与 SP 之间要交接一句话——"这个人是谁"。麻烦在于:这句话要跨过一个不可信的网络,而且不能每次请求都回头问 IdP。
两个现象:
① 应用拿到一段"用户是 alice"的文字,却没有办法证明这段文字没被人改过
② 如果每次请求都要回头问 IdP "他说的是真的吗",IdP 立刻变成整条链路最忙的那个点
它面对的需求是:让这段结论既能被接收方自己验证真伪,又不必回源。
答案不是"把结论藏起来"(藏起来接收方就没法用),而是让它带着一份只有 IdP 做得出来的证明。
二、它是什么
一句话:断言是"IdP 说这个人是谁"的那句话;令牌是这句话的载体。
在 OIDC 里,断言通常就装在两段令牌里:ID Token 的载荷是"给这个应用看的身份结论",access token 的载荷是"给资源方看的通行依据"。本页说的"令牌"主要指前者。
它不是下面三样东西,混淆会让判断整体走偏:
- 不是"加密过的内容"。 令牌的中间那一段谁都能读——它是 base64url 编码,不是加密。签名段不是加密段:签名只保证"没人改过",不保证"没人看过"。
- 不是"一把需要回头查表的钥匙"。 结论本身就在令牌里(
sub、preferred_username、email都在载荷里),接收方解码就能读,不需要先问 IdP。 - 不是"永远有效的凭证"。
exp写在载荷里,过了那个时间点它就是一张废纸——access token 的expires_in是 300 秒。
三、它长什么样
它是一个用两个点分成的三段串。这是 ID Token(环境:Keycloak 26.8.1,realm = sso-lab):
header = eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI2c0tPcnRLd2tweWNEQVFid1p6cFJLSkVFMDRiNS1aeTN3Q29DVUkwZmY0In0
payload = eyJleHAiOjE3OTIwNTAxMjYsImlhdCI6MTc5MjA0OTgyNiwiYXV0aF90aW1lIjoxNzkyMDQ5ODI2LCJqdGkiOiIxOGJiZjdlMC02MWQ2LTM4MzItMmE1MS0yYmZmOGM1MDdmMDIiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODEvcmVhbG1zL3Nzby1sYWIiLCJhdWQiOiJzcC1zcGEiLCJzdWIiOiI4YjBlMzNhNy01ZTViLTQwNTEtYTIwZS00MTdmYTViMWNmNWQiLCJ0eXAiOiJJRCIsImF6cCI6InNwLXNwYSIsIm5vbmNlIjoiX01KanZ3UFlEb29FV3V0VWt5aWh1QSIsInNpZCI6IlZuWGtzeWpoZ0pleGNzTE9vT0U1ZmM4YyIsImF0X2hhc2giOiJsTUN1UVE2cE1qajl5aU1GdzhaMnFRIiwiYWNyIjoiMSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJuYW1lIjoiQWxpY2UgQWNtZSIsInByZWZlcnJlZF91c2VybmFtZSI6ImFsaWNlIiwiZ2l2ZW5fbmFtZSI6IkFsaWNlIiwiZmFtaWx5X25hbWUiOiJBY21lIiwiZW1haWwiOiJhbGljZUBleGFtcGxlLmNvbSJ9
signature = mHY-JWwQobkYpwRS25nAPJwsfCfht9yrYJ9kBY54cNExuv2C3FyVATw30cT78jyCsp6-J0t3A5SzIFhG82CmTcllAv1y8j2iLmtKpAUtunm4GnGy93sWBTXVfYX67uQeIGGNJcBca7Nb1ab3punzzsjEwdtaBj5ewOljknpDUlciBhKFpw8BOYw-BUxpLJkZWi572eEu5CNeybThhNuI6B_qALV3RQ0O0d_GO71ni5rcmmtKKBhkHRTOwKbdzZWkGFMe2AdBAFNtDkBzRQIvvO-0VMv2D5zAJHvW7Z1DdfGb9H_8pavGPXlqp4lUrobEztcsq2Aj6Kn3PGirA2MR4Q
三段各管一件事:
| 段 | 里面是什么 | 谁读它 |
|---|---|---|
| header | alg(用什么算法签的)、typ、kid(用哪把公钥) |
校验方 |
| payload | 断言本身:iss(谁说的)、sub(说的是谁)、aud(说给谁听)、exp |
谁都能读 |
| signature | 对前两段算出的签名 | 校验方 |
把中间那段解出来(节选,值照录):
{
"iss": "http://localhost:8081/realms/sso-lab",
"sub": "8b0e33a7-5e5b-4051-a20e-417fa5b1cf5d",
"aud": "sp-spa",
"exp": 1792050126,
"auth_time": 1792049826,
"nonce": "_MJjvwPYDooEWutUkyihuA",
"sid": "VnXksyjhgJexcsLOoOE5fc8c",
"at_hash": "lMCuQQ6pMjj9yiMFw8Z2qQ",
"preferred_username": "alice",
"email": "alice@example.com"
}
签名段不是加密段,还有一处旁证:IdP 公布的公钥里,签名和加密是分开的两把。同一个 IdP 的 JWKS:
kid = 6sKOrtKwkpycDAQbwZzpRKJEE04b5-Zy3wCoCUI0ff4 | kty = RSA | alg = RS256 | use = sig
kid = Wm7uV1Y2D6Xz1nADFcVsnGmcQWkOCvw39w-bOeYWhjo | kty = RSA | alg = RSA-OAEP | use = enc
at_hash 是另一处容易漏看的字段:它把 access token 的摘要绑进 ID Token,防止两段令牌被分开替换。nonce 是发起登录时 SP 自己给的,用来证明"这段断言对应我这次请求",sid 对应 IdP 侧的那次会话(04 页 会用到)。
四、它怎么工作
一段断言从"说出口"到"被接受"是四步:
① 写句话 IdP 把断言整理成 JSON —— 这就是 payload
② 盖上章 用私钥对 header + payload 算一次签名,拼成三段
③ 验章 应用拿 IdP 公布的公钥(JWKS)验签名:内容被动过,签名就对不上
④ 对口径 签名过了才读 payload,再比 iss / aud / exp 是否对得上
第 ③④ 步都不需要联网问 IdP——公钥可以提前缓存。这就是"既能自证、又不必回源"的落点;代价是签出去就收不回来(exp 之前一直有效)。
五、它会带出哪些概念
- 授权码与 PKCE:令牌不是凭空掉下来的,得先拿一个一次性的
code去换。→ 03 页 - 重定向与回跳:令牌里那个
iss,和回跳地址里带回来的iss是同一件事。→ 02 页 - 会话与单点登出:令牌只有 300 秒,而人不会 300 秒登一次——中间撑住的是会话。→ 04 页
六、回到应用课
| 应用课里的那句话 | 现在你知道它为什么成立 |
|---|---|
| "都叫 token,所以能互相替代" | 三种 token 各有归属方与读者:ID Token 的 aud 是 sp-spa,access token 的 aud 是 account——写给不同人的两封信 |
| "拿身份结论去调接口" | ID Token 是给应用看的身份结论,不是给资源方看的通行依据;typ 一项就能把两者分开 |
| "签名验过了,这次登录就合法了" | 签名只证明"内容确实是 IdP 签过";还要逐个比对 iss / aud / exp / nonce / at_hash——at_hash 就是把 access token 绑进 ID Token 的那根绳 |
这一页改变了你的哪个判断:拿到一段令牌时,你不再只关心"它能不能用",而是先去解码载荷,问三件事——iss 是不是我信的那个 IdP、aud 是不是我这个应用、exp 还剩多久;三问里任何一问不对,签名再真也不能用。
→ 这一页的展开在应用课 03 章 · 令牌与校验。
自测题
- 两段令牌,一段的
aud是sp-spa,另一段的aud是account。哪一段是发给这个应用的?另一段发给谁看? - "签名段不是加密段"——那么把别人的 ID Token 拿过来,你能读出他的用户名吗?这说明了什么?