令牌吊销 — introspection、revocation、阻止列表
目标
按 RFC 7009 撤销 Keycloak 签发的令牌,并用 RFC 7662 introspection 确认结果,然后用真实请求证明:在资源服务器上,本地验证、introspection、jti 黑名单这三种方式对已撤销的令牌分别如何反应。
为什么重要
签名的 JWT 由资源服务器自己验证,所以即使在签发方结束了会话,API 也不知道这件事,令牌到过期前都能通过。RFC 7009 也写明,自包含令牌要立即撤销需要另外的联动,替代方案是短寿命。于是设计者要在三者中选择——每次询问签发方的 introspection(即时,代价是往返和可用性耦合)、资源服务器的黑名单(即时,代价是状态存储)、短 TTL(简单,代价是最长延迟一个令牌寿命)。本实验把三种方式并排放在一个服务器上,亲自测量一个已撤销的令牌在各自那里是 200 还是 401,以及这个代价是多少毫秒。
步骤
- 如果 Keycloak 是关着的,就用
lab-start-keycloak启动,并用/root/st/revoke/wait.sh等到http://127.0.0.1:8080/realms/labhub返回 200 为止(最多 240 秒)。把耗时按ready_seconds=<정수>(占位符为整数)写入/root/st/revoke/ready.txt。 - 用 source 加载
/opt/fixtures/kc/realm-info.env,通过公共客户端web-app的密码授权模式(用户dev1)获取令牌,并把完整的响应正文 JSON 保存到/root/st/revoke/token.json。 - 让
/root/st/revoke/introspect.sh <토큰>(占位符为令牌)以api-svc的身份调用 introspection 端点,并原样输出响应 JSON。密钥从 env 中读取。新令牌是active=true,伪造的是active=false。 - 让
/root/st/revoke/revoke.sh <refresh 토큰>(占位符为 refresh 令牌)向 RFC 7009 撤销端点发送client_id=web-app、token、token_type_hint=refresh_token,并把撤销前后的结果按before=true、after=false、refresh_after=400写入/root/st/revoke/revoke.txt。 - 在 127.0.0.1:8307 上启动
/root/st/revoke/api.py(/health、只用 JWKS 验证的/local/me、每次请求都做 introspection 的/introspect/me)。用/root/st/revoke/bench.py对两条路径各测 10 次以上,把n、local_ms、introspect_ms、access_ttl、max_exposure_s写入/root/st/revoke/latency.txt。 - 给
api.py加上GET /guarded/me(检查黑名单revoked:jti:<jti>)和POST /logout(把 jti 按剩余寿命放进黑名单,并在 Keycloak 中撤销 refresh),然后重新启动。 - 用
/root/st/revoke/e2e.sh测试整个流程,在/root/st/revoke/e2e.out中留下七行。
参考
- Realm 地址、客户端、用户、密钥用
lab-env或cat /opt/fixtures/kc/realm-info.env查看。不要把值写进脚本,用source加载。 - 撤销端点对无效令牌也返回 200。是否生效,用 introspection 来确认。
- 常见错误 1:只撤销访问令牌——在这个 Keycloak 中 refresh 仍然有效,会不断得到新的访问令牌。退出登录必须撤销 refresh。
- 常见错误 2:黑名单条目不设 TTL——连已过期的令牌也会永远堆积。只记住剩余寿命就够了。
启动 Keycloak 并等待 Realm 就绪
如果 Keycloak 是关着的,就用 lab-start-keycloak 启动,并用 /root/st/revoke/wait.sh 等到 http://127.0.0.1:8080/realms/labhub 返回 200 为止(最多 240 秒)。把耗时按 ready_seconds=<정수>(占位符为整数)写入 /root/st/revoke/ready.txt。
Keycloak 是 JVM,所以端口打开之后,还要再花时间加载 Realm。不要用固定的 sleep,而要用 while 循环轮询状态码。是否已启动,用 lab-status 查看。
用 web-app 获取 dev1 的令牌对
用 source 加载 /opt/fixtures/kc/realm-info.env,通过公共客户端 web-app 的密码授权模式(grant_type=password,用户 dev1)获取令牌,并把完整的响应正文 JSON 保存到 /root/st/revoke/token.json。其中必须有 access_token、refresh_token、expires_in。
令牌端点是 env 中的 KC_TOKEN_URL。公共客户端不带密钥,只发送 client_id。密码里有特殊字符,请使用 --data-urlencode。
用 introspection 确认 active
让 /root/st/revoke/introspect.sh <토큰>(占位符为令牌)以机密客户端 api-svc 的身份调用 RFC 7662 introspection 端点(/realms/labhub/protocol/openid-connect/token/introspect),并原样输出响应 JSON。不要把密钥写进文件,而要从 realm-info.env 中读取。新令牌必须得到 active=true,伪造的字符串必须得到 active=false。
introspection 必须认证调用方(防止令牌扫描)。用 curl -u 提供 Basic 认证,并以 form 形式 POST token。对于无效令牌,除了 active 之外不返回任何信息才是正常的。
用 RFC 7009 撤销 refresh 令牌
让 /root/st/revoke/revoke.sh <refresh 토큰>(占位符为 refresh 令牌)向 RFC 7009 撤销端点(/realms/labhub/protocol/openid-connect/revoke)发送 client_id=web-app、token、token_type_hint=refresh_token。用新的令牌对,把撤销前后 access 令牌的 introspection 结果和撤销后 refresh 授权的结果,按 before=true、after=false、refresh_after=400 写入 /root/st/revoke/revoke.txt。
撤销端点无论成功还是无效令牌都返回 200,所以仅凭响应码无法知道是否生效。请用 introspection 确认结果。即使是公共客户端,漏掉 client_id 也会被拒绝。
本地验证与 introspection——撤销和延迟
在 127.0.0.1:8307 上启动 /root/st/revoke/api.py。GET /health 返回 200,GET /local/me 只用 JWKS 验证 Bearer 令牌(签名、exp、iss、aud=lab-api),GET /introspect/me 每次请求都以 introspection 的 active 来判定(两者都是成功 200、失败 401)。然后用 /root/st/revoke/bench.py 对两条路径用同一个令牌测量 N 次以上(10 次以上),把 n=、local_ms=、introspect_ms=、access_ttl=(令牌的 exp-iat)、max_exposure_s=(只做本地验证时,已撤销的令牌最多还能存活的秒数)写入 /root/st/revoke/latency.txt。
PyJWT 的 PyJWKClient 会获取并缓存 JWKS。本地路径不向签发方询问,所以已撤销的令牌也会在过期之前一直返回 200——在这一步这是正确的结果,这个窗口的长度就是令牌寿命。如果缓存 introspection 的结果,撤销就会延迟生效。
jti 黑名单与退出登录
给 /root/st/revoke/api.py 加上 GET /guarded/me(本地验证之后,如果存在 Redis 键 revoked:jti:<jti> 就返回 401)和 POST /logout(把 Bearer 令牌的 jti 以 revoked:jti:<jti> 为键、按令牌的剩余寿命设置 TTL 保存,并按 RFC 7009 在 Keycloak 中撤销 form 正文里的 refresh_token,然后返回 200),然后重新启动。退出登录后,同一个令牌访问 /guarded/me 必须返回 401。
TTL 是 exp 减去当前时间。过期的令牌在签名验证时本来就会被拒绝,所以之后没有理由再记住。只靠黑名单的话,Keycloak 一侧的会话仍然存活,用 refresh 还能拿到新令牌,所以撤销请求也要一并发送。修改代码后必须停掉服务器再重新启动。
用一个流程证明撤销策略
用 /root/st/revoke/e2e.sh 获取新令牌,并按 introspection → /guarded/me → POST /logout → /guarded/me、/local/me → introspection → refresh 授权的顺序测试,在 /root/st/revoke/e2e.out 中逐行留下 introspect_before=true、guarded_before=200、logout=200、guarded_after=401、local_after=200、introspect_after=false、refresh_after=400。
直接使用前面步骤的 introspect.sh 和服务器。评分器不只看文件,还会重新运行一次 e2e.sh,查看是否得到同样的七行。local_after 为 200,正是短 TTL 要弥补的窗口。