KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
03 · 授权码与 PKCE — keel 龙骨
类型:衍生概念(服务于「重定向与回跳」) · 应用课对应:02 章 · 授权码流与 PKCE
类型:衍生概念(服务于「重定向与回跳」) · 应用课对应:02 章 · 授权码流与 PKCE
这一页只回答四件事:它为什么会出现、它是什么、它长什么样、它怎么工作。它不讲怎么实现一个授权服务器。
一、它为什么会出现
回跳是经过浏览器走的(02 页),而浏览器经过的每一站都会留痕:地址栏历史、代理日志、服务器的访问日志、Referer 头。
两个现象:
① 如果回跳地址里直接带 access_token,它就会进浏览器历史、进每一级访问日志
② 谁从日志里捡到那段串,谁就能冒充这个人调接口
它面对的需求是:让"经过浏览器的那一段"里,不含任何拿到手就能用的凭据。
答案是把凭据拆成两半:经过浏览器的只是一张兑不出来任何东西的凭据,真正能用的东西由应用在背后换。
二、它是什么
一句话:授权码是 IdP 发出的一张"兑换凭据"——它本身调不了任何接口,只能用一次,且必须由应用在背后换成令牌。
而 PKCE 是给这张凭据配的一把锁:code_verifier 是发起登录的那一方自己造的随机串,code_challenge 是它的摘要;只有同时握着这两个里应外合的值的那个应用,才能把 code 换成令牌。
它不是下面三样东西:
- 不是令牌。 拿
code去调业务接口什么也调不到;它的唯一用途是去换令牌。 - 不是"可以慢慢用的一张票"。 用一次就作废,复用直接报
Code not valid——而且失败的那一次也算用过。 - 不是"PKCE 只是给移动端用的可选加固"。 对没有
client_secret的应用(SPA、移动端)来说,PKCE 就是它证明"我是刚才发起登录的那个应用"的唯一手段。
三、它长什么样
一次真实登录里,这两个值由应用在发授权请求之前就生成好(环境:Keycloak 26.8.1,realm = sso-lab):
code_verifier = SsVH2Ja25l4B-VkTksOhmYU7qrBJU1pQ-pRfywZv470
code_challenge = 8Vklj4QX3rXa9CurOob2v3rc6v8ei2wZzj6s7Uij7tY (= S256(verifier))
回跳带回来的那个 code(经过浏览器的那一段):
code = 44f80113-b402-a38e-bbc0-56310447ebf8.VnXksyjhgJexcsLOoOE5fc8c.74717848-0bf1-4f44-81f2-e31422eb12c8
IdP 侧支持两种摘要方式,登记在发现文档里:
code_challenge_methods_supported = ['plain', 'S256']
两个值归谁管,是这一页最该记住的一张表:
| 值 | 谁生成 | 谁保存 | 什么时候拿出来 |
|---|---|---|---|
code_verifier |
发起登录的应用 | 只有应用自己 | 换令牌那一刻 |
code_challenge |
应用(对 verifier 求摘要) | 随授权请求发给 IdP | IdP 收到时就用它把这次登录记下来 |
兑换失败时 IdP 给的原话:
# 用被篡改的 verifier 去换
400 {"error":"invalid_grant","error_description":"PKCE verification failed: Code mismatch"}
# 同一个 code 再用一次(这次 verifier 是对的)
400 {"error":"invalid_grant","error_description":"Code not valid"}
第二行是一个反直觉的地方:被篡改的 verifier 失败的那一次,也已经把 code 消耗掉了。 所以"用错 verifier 失败后、再用对的 verifier 重试"是拿不到令牌的——重试必须从授权请求重新开始。
四、它怎么工作
① 生成 应用造一个 code_verifier,对它求一次摘要得到 code_challenge
② 带上 授权请求里带上 code_challenge;IdP 把它跟这次登录绑在一起记下
③ 回跳 浏览器带回 code —— 它在地址里,但单独它什么也换不出
④ 兑换 应用在背后用 code + code_verifier 换令牌;IdP 现算一次摘要,跟 ② 记下的比
PKCE 防的那一类攻击在第 ③④ 步之间:别人从日志或 Referer 里捡到了 code,但他没有 code_verifier,摘要对不上,换不出令牌。
而 code 一次性的属性,防的是另一件事:即使这张凭据泄了,它的可用窗口也只有一次。 两个性质合起来,才把"经过浏览器的东西"压到了无害。
五、它会带出哪些概念
- 重定向与回跳:
code是怎么穿过浏览器的,以及回跳里另外两个参数(state、iss)各自管什么。→ 02 页 - 断言与令牌:第 ④ 步换回来的东西就是断言与令牌。→ 01 页
- 会话与单点登出:换来的 access token 只有 300 秒寿命,撑住后续访问的是应用侧会话。→ 04 页
六、回到应用课
| 应用课里的那句话 | 现在你知道它为什么成立 |
|---|---|
| "PKCE 到底防住了什么" | 它让 code 只对"发起这次登录的那个应用"有用:别人截到 code 也没有 code_verifier,摘要对不上 |
"应用不该从 code 里面读任何东西" |
code 不是结论,只是一张兑换凭据;结论在换回来的 ID Token 载荷里 |
"PKCE 失败那次也消耗了 code" |
兑换无论成败都算一次消耗;所以重试必须从授权请求那一头重新走 |
这一页改变了你的哪个判断:看到回跳里那个 code,你不再只当它是一段"马上去兑换的字符串",而是先问**"这次兑换要用的 code_verifier 在谁手上、还在不在"**——verifier 丢了,code 再新也换不出东西。
→ 这一页的展开在应用课 02 章 · 授权码流与 PKCE。
自测题
code_verifier与code_challenge分别是谁生成、谁保存、什么时候拿出来?- 一次兑换因为 verifier 不对而失败,为什么"再拿正确的 verifier 重试"也拿不到令牌?