KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA
03 · 把自己的东西装进去 — keel 龙骨
前两章的 IdP 还是空的——它不知道有"你的应用"这回事。这一章做两件事: 在 IdP 里登记你的应用,然后亲眼走一遍"跳去登录、再跳回来"。
前两章的 IdP 还是空的——它不知道有"你的应用"这回事。这一章做两件事:
在 IdP 里登记你的应用,然后亲眼走一遍"跳去登录、再跳回来"。
先认两个词:
- realm(领域):IdP 里的一个"圈",有自己的用户、客户端、配置,互相看不到。
它的名字就在地址里——http://localhost:8081/realms/**sso-lab**/…斜杠中间那段就是 realm 名。
前面用的master是 Keycloak 自带的管理圈,你的应用该住自己建的圈。 - client(客户端):在 IdP 里为"每一个要接进来的应用"登记的一条记录。
登记时最要紧的是约定 redirect_uri 白名单——"跳回来时允许落到哪个地址"。
一、建一个 realm,再登记一个 client
走控制台(点出来的)
- 浏览器打开
http://localhost:8081/admin/,用admin/admin登录; - 左上角那个下拉框里点 Create realm → Realm name 填
sso-lab→ Create; - 左侧菜单 Clients → Create client → Client ID 填
my-app→ Next
→ Client authentication 保持 Off(前台应用,没有后端替它保管密钥)
→ Next → Valid redirect URIs 填http://localhost:8090/callback→ Save; - 顺手建个用户:左侧 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
会发生三件事:
- 先跳到一个登录页——标题里带着你的 realm 名,只有用户名和密码两个框。
这个页面由 IdP 提供,你的应用上没有这一页; - 用
alice/alice-pass-1登录; - 浏览器跳回
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当可信来源、会照常回传,多数脚本库不会。
这是脚本与浏览器的差异,不是"浏览器也登不上"。
四、它不是什么
code不是 token,callback也不是"登录成功页"。 它是一个交接点:
浏览器在这里把号码牌交给你的应用,应用再拿它去后端换凭证。看不懂这一点,
就会写出"把 code 当 token 用"的代码。- 登记一个 client,不是在 IdP 里"建用户"。 用户属于 realm(IdP 侧,
alice就在这儿);
client 只是"哪一个应用被允许来问"。一个 realm 可以挂很多个 client、共享同一批用户
——这正是"一个账号进多个系统"的地基。
读完你能做什么
- 在 IdP 里建一个 realm、登记一个 client,并说清这两者分别是什么、各自在哪;
- 亲手走完"跳去登录 → 登录 → 跳回来",并看懂回跳地址里每个参数是什么意思;
- 说清那条最容易错的:
code不是 token,且只能用一次。
下一步:你已经把一个应用接进去了。这门课最后一章换个方向——
先说清什么情况下别用它,再告诉你往下该去哪。