KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

03 · 把自己的东西装进去 — keel 龙骨

前两章的 IdP 还是空的——它不知道有"你的应用"这回事。这一章做两件事: 在 IdP 里登记你的应用,然后亲眼走一遍"跳去登录、再跳回来"。

前两章的 IdP 还是空的——它不知道有"你的应用"这回事。这一章做两件事:
在 IdP 里登记你的应用,然后亲眼走一遍"跳去登录、再跳回来"。

先认两个词:

一、建一个 realm,再登记一个 client

走控制台(点出来的)

  1. 浏览器打开 http://localhost:8081/admin/,用 admin / admin 登录;
  2. 左上角那个下拉框里点 Create realm → Realm name 填 sso-lab → Create;
  3. 左侧菜单 Clients → Create client → Client ID 填 my-app → Next
    → Client authentication 保持 Off(前台应用,没有后端替它保管密钥)
    → Next → Valid redirect URIs 填 http://localhost:8090/callback → Save;
  4. 顺手建个用户:左侧 Users → Add user → Username 填 alice → Create
    → 切到 Credentials 标签 → Set password 填 alice-pass-1,Temporary 关掉 → Save。

走 REST(敲出来的)

控制台点出来的每一步,背后都是一次 REST 调用。先换一个管理用的令牌:

TOKEN=$(curl -s -X POST http://localhost:8081/realms/master/protocol/openid-connect/token \
  -d grant_type=password -d client_id=admin-cli -d username=admin -d password=admin \
  | python -c "import sys,json;print(json.load(sys.stdin)['access_token'])")
echo "token 长度 = ${#TOKEN}"
token 长度 = 750

(数字不必一样——能当 Bearer 用就说明拿到了。)然后三步,每步都看返回的状态码:

# 1) 建 realm
curl -s -o /dev/null -w "realm=%{http_code}\n" -X POST http://localhost:8081/admin/realms \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"realm":"sso-lab","enabled":true,"displayName":"SSO Lab"}'

# 2) 登记 client
curl -s -o /dev/null -w "client=%{http_code}\n" -X POST http://localhost:8081/admin/realms/sso-lab/clients \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"clientId":"my-app","publicClient":true,"protocol":"openid-connect","standardFlowEnabled":true,"redirectUris":["http://localhost:8090/callback"]}'

# 3) 建用户并设密码
curl -s -o /dev/null -w "user=%{http_code}\n" -X POST http://localhost:8081/admin/realms/sso-lab/users \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"username":"alice","enabled":true,"emailVerified":true}'
UIDV=$(curl -s -H "Authorization: Bearer $TOKEN" \
  "http://localhost:8081/admin/realms/sso-lab/users?username=alice&exact=true" \
  | python -c "import sys,json;print(json.load(sys.stdin)[0]['id'])")
curl -s -o /dev/null -w "password=%{http_code}\n" -X PUT \
  "http://localhost:8081/admin/realms/sso-lab/users/$UIDV/reset-password" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"type":"password","value":"alice-pass-1","temporary":false}'

预期形状:

realm=201
client=201
user=201
password=204

201 = 建出来了,204 = 改成功了。已经建过再跑一遍会得到 409(已存在)——
说明这一步你做过了,不是错。

二、把它打开

现在把这个网址粘进浏览器地址栏:

http://localhost:8081/realms/sso-lab/protocol/openid-connect/auth?client_id=my-app&redirect_uri=http%3A%2F%2Flocalhost%3A8090%2Fcallback&response_type=code&scope=openid+profile+email&state=demo-state

会发生三件事:

  1. 先跳到一个登录页——标题里带着你的 realm 名,只有用户名和密码两个框。
    这个页面由 IdP 提供,你的应用上没有这一页;
  2. 用 alice / alice-pass-1 登录;
  3. 浏览器跳回 redirect_uri,地址栏变成下面这个样子。

页面本身会显示"无法访问此网站"(localhost:8090 上没有服务在听)。不用管页面,看地址栏:

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 一张一次性号码牌。后面拿它去换真正的凭证(见下一节)
state 你在授权 URL 里发出去的值,原样回来——用来确认这次是你发起的那次
session_state IdP 侧这次会话的标识(同一段会话里多个应用共享它)
iss 谁发的这张回跳(http://localhost:8081/realms/sso-lab,%3A 就是 :)

上面是真实的回跳:code、state、session_state 每跑一次都不同——
要读的是"有哪几个参数、各自代表什么",不是照抄那串字符。

三、最容易看错的一点

地址栏里的 code,不是 token。

它只是一张号码牌:证明"这个人刚才在 IdP 那边登录成功了",它本身不是能拿去访问接口的凭证。
真正的凭证(token)要用两样东西换:这个 code + 只有你的应用才知道的一串密钥。
换的地方是清单里的 token_endpoint——这一步必须由后端做,因为密钥不能放进浏览器。

顺带一个几乎人人会踩的事实:code 只能用一次。

400 {"error":"invalid_grant","error_description":"Code not valid"}

拿同一个 code 去换第二次,就会看到这句话。所以"没收到 token 就再试一次"是行不通的
——第一次尝试(哪怕失败)也已经把号码牌用掉了。

如果你不用浏览器、改用脚本模仿这一段:提交密码那一步可能收到 cookie_not_found。
原因:Keycloak 在 http://localhost 下仍给会话 cookie 加 Secure; SameSite=None——
浏览器把 http://localhost 当可信来源、会照常回传,多数脚本库不会。
这是脚本与浏览器的差异,不是"浏览器也登不上"。

四、它不是什么

读完你能做什么

下一步:你已经把一个应用接进去了。这门课最后一章换个方向——
先说清什么情况下别用它,再告诉你往下该去哪。

进入 keel 阅读