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 的载荷是"给资源方看的通行依据"。本页说的"令牌"主要指前者。

它不是下面三样东西,混淆会让判断整体走偏:

三、它长什么样

它是一个用两个点分成的三段串。这是 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 之前一直有效)。

五、它会带出哪些概念

六、回到应用课

应用课里的那句话 现在你知道它为什么成立
"都叫 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 章 · 令牌与校验。

自测题

  1. 两段令牌,一段的 aud 是 sp-spa,另一段的 aud 是 account。哪一段是发给这个应用的?另一段发给谁看?
  2. "签名段不是加密段"——那么把别人的 ID Token 拿过来,你能读出他的用户名吗?这说明了什么?

进入 keel 阅读