KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

02 · 重定向与回跳 — keel 龙骨

类型:主题概念 · 应用课对应:02 章 · 授权码流与 PKCE

类型:主题概念 · 应用课对应:02 章 · 授权码流与 PKCE

这一页只回答四件事:它为什么会出现、它是什么、它长什么样、它怎么工作。它不讲怎么配客户端。

一、它为什么会出现

应用需要 IdP 给的那段结论,但它不能自己去要:它一旦能自己去要,就意味着它得先拿到用户的密码。

于是出现了一个很别扭的安排:让用户眼前的浏览器替应用跑一趟。 麻烦也全在这里——浏览器会留下痕迹,而这一趟里带的东西必须经得起被别人看见。

两个现象:

① 点了"用 SSO 登录",地址栏跳到 IdP,输完密码,跳回来 —— 页面一片空白
② 浏览器地址栏里明明躺着 ?state=…&iss=…&code=…,应用却像是没收到

它面对的需求是:把"结论"从 IdP 带回应用,而这趟路全程不许出现密码,也不许出现能被直接使用的凭据。

二、它是什么

一句话:重定向是"应用把浏览器交给 IdP"的那一跳;回跳是"IdP 把浏览器连同结论交回应用"的那一跳。两跳都由浏览器亲自走。

它不是下面三样东西:

三、它长什么样

第一跳,一次真实的授权请求(环境:Keycloak 26.8.1,dev 模式,realm = sso-lab):

http://localhost:8081/realms/sso-lab/protocol/openid-connect/auth
  ?client_id=sp-spa
  &redirect_uri=http%3A%2F%2Flocalhost%3A8090%2Fcallback
  &response_type=code
  &scope=openid+profile+email
  &state=AsoTwZFFJyDK-urhFMy_jw
  &nonce=_MJjvwPYDooEWutUkyihuA
  &code_challenge=8Vklj4QX3rXa9CurOob2v3rc6v8ei2wZzj6s7Uij7tY
  &code_challenge_method=S256

第二跳,IdP 交回浏览器时给的那一行(HTTP 响应头原文):

status = 302
Location = http://localhost:8090/callback?state=AsoTwZFFJyDK-urhFMy_jw&session_state=VnXksyjhgJexcsLOoOE5fc8c&iss=http%3A%2F%2Flocalhost%3A8081%2Frealms%2Fsso-lab&code=44f80113-b402-a38e-bbc0-56310447ebf8.VnXksyjhgJexcsLOoOE5fc8c.74717848-0bf1-4f44-81f2-e31422eb12c8

回跳带回来的参数是四个:code、iss、session_state、state。其中两个是有防御职责的:

参数 防的是什么 比对的是"谁"
state 别人伪造一次回跳,让你的应用去换令牌 比对自己这次发起时发出去的那个值
iss Mix-Up:多个 IdP 之间被掉包 比对自己登记过的那个 IdP 地址

iss 是 RFC 9207 定义的"授权响应发行者标识",它和令牌里的 iss 是同一个值(01 页)。回跳地址里带 iss,是为了让应用在换令牌之前就能判断"这个响应是不是我信的那个 IdP 发的"。

如果那个回跳地址没登记过,IdP 不会把结论交回来:

status = 400
页面上的错误 = Invalid parameter: redirect_uri

四、它怎么工作

一次完整的来回是四步:

① 应用生成 state(以及 nonce),把浏览器重定向到 IdP 的授权端点
   ↓
② 浏览器在 IdP 的地址上完成登录      ← 密码只在这里出现
   ↓
③ IdP 把浏览器重定向回预先登记的 redirect_uri,带上 code / state / iss
   ↓
④ 应用当场核 state 与 iss;都对上,才拿 code 去换令牌

第 ③ 步是整件事的枢纽:这一趟经过浏览器的地址里,只有 code 是"凭据类"的东西,而它一次就用废(03 页)。真正能用的令牌在第 ④ 步背后换,不进地址栏。

第 ① 步发出去的 state 与第 ④ 步收回来的 state 必须是同一个值——这是应用唯一能验证"这次回跳是我要的那一次"的手段。

五、它会带出哪些概念

六、回到应用课

应用课里的那句话 现在你知道它为什么成立
"回跳回来一片空白" 空白常常是回跳回来过了,只是应用的处理里没把 code 换成令牌;问题不在"没回来",而在"回来了没人接"
"state 与 iss 是两个容易被当成'装饰'的参数" 它们各自证明一件事:state 证明这次回跳对应我发起的那一次,iss 证明响应来自我登记的那个 IdP
"redirect_uri 是结论送回去的正确地址,送错地方等于把身份送给别人" 正因为它是一个"送回去的地址",IdP 只肯把它交给预先登记过的那一个;没登记的地址一律 400

这一页改变了你的哪个判断:看到回跳地址里那一长串参数时,你不再只当它是系统自带的噪声,而是先去比对 state 是不是自己发出去的那个、iss 是不是自己登记的那个 IdP——这两项任何一项对不上,这次回跳就不能用。

→ 这一页的展开在应用课 02 章 · 授权码流与 PKCE 与 04 章 · 会话与登出。

自测题

  1. state 与 iss 各防的是哪一类问题?各自比对的是"谁"?
  2. 一个没登记过的 redirect_uri 会被怎么处理?从这一条看,它更像一个配置项,还是一条安全边界?

进入 keel 阅读