11 — JWT 签名、校验
📦 GitHub: https://github.com/geekchow/micro-service-auth
本 PoC 中令牌如何被签名、校验与内省 —— 以及为什么即便对一个结构合法、尚未过期的 JWT,Keycloak 也可能返回 active: false。
全文使用的固定术语见 01 — 概念:Keycloak(IdP)、Kong(PEP)、OPA(PDP)、banking-api-service(资源服务器)。
四个要素
1. 签名
签名是「Keycloak 创建了该令牌」的密码学证明。
它是在 Keycloak 签发访问令牌时,用 Keycloak 的私钥对令牌的 header 与 payload 签名而产生的。
为什么重要:
- 它证明令牌来自 Keycloak
- 它证明 payload 在签发后未被改动
- 它防止调用方仅通过 Base64URL 编码数据就伪造出一个看似有效的假令牌
2. 校验(Validation)
校验是指检查令牌是真实的、且对本次请求是可接受的。
典型的校验项有:
- 签名有效
- 令牌未过期(exp)
- 签发者(iss)与期望的 Keycloak realm 匹配
- 受众(aud)与该 client 或 API 匹配
在本 PoC 中:
- banking-api-service 使用 JWKS 在本地校验 JWT
- Kong 在信任令牌做策略决策之前使用 Keycloak 内省
3. 内省(Introspection)
内省是从某个服务向 Keycloak 内省端点发起的一次实时调用。
Keycloak 回复令牌是否 active,也可能返回令牌元数据。
为什么重要:
- 它从「真相之源」确认令牌仍然有效
- 它能捕获被吊销的或其他 inactive 的令牌
- 相比仅解码 JWT,它给出更强的信任检查
在本 PoC 中,Kong 在用解码后的声明为 OPA 构建策略 input 之前,先对令牌做内省。
关于「为什么即便对一个结构合法、未过期的 JWT,Keycloak 也可能返回 active: false」的完整机制,详见下文「内省实际检查什么」一节。
4. 声明(Claims)
声明是 JWT payload 内部的数据。
本 PoC 中的示例:
- iss
- aud
- preferred_username
- realm_access.roles
- customer_id
- account_ids
解码令牌让你能读取这些声明。 校验或内省告诉你这些声明是否可信。
信任链如何工作
#mermaid-svg-flxbZVlAofzwm3E0{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-flxbZVlAofzwm3E0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-flxbZVlAofzwm3E0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-flxbZVlAofzwm3E0 .error-icon{fill:#552222;}#mermaid-svg-flxbZVlAofzwm3E0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-flxbZVlAofzwm3E0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-flxbZVlAofzwm3E0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-flxbZVlAofzwm3E0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-flxbZVlAofzwm3E0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-flxbZVlAofzwm3E0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-flxbZVlAofzwm3E0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-flxbZVlAofzwm3E0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-flxbZVlAofzwm3E0 .marker.cross{stroke:#333333;}#mermaid-svg-flxbZVlAofzwm3E0 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-flxbZVlAofzwm3E0 p{margin:0;}#mermaid-svg-flxbZVlAofzwm3E0 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-flxbZVlAofzwm3E0 .cluster-label text{fill:#333;}#mermaid-svg-flxbZVlAofzwm3E0 .cluster-label span{color:#333;}#mermaid-svg-flxbZVlAofzwm3E0 .cluster-label span p{background-color:transparent;}#mermaid-svg-flxbZVlAofzwm3E0 .label text,#mermaid-svg-flxbZVlAofzwm3E0 span{fill:#333;color:#333;}#mermaid-svg-flxbZVlAofzwm3E0 .node rect,#mermaid-svg-flxbZVlAofzwm3E0 .node circle,#mermaid-svg-flxbZVlAofzwm3E0 .node ellipse,#mermaid-svg-flxbZVlAofzwm3E0 .node polygon,#mermaid-svg-flxbZVlAofzwm3E0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-flxbZVlAofzwm3E0 .rough-node .label text,#mermaid-svg-flxbZVlAofzwm3E0 .node .label text,#mermaid-svg-flxbZVlAofzwm3E0 .image-shape .label,#mermaid-svg-flxbZVlAofzwm3E0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-flxbZVlAofzwm3E0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-flxbZVlAofzwm3E0 .rough-node .label,#mermaid-svg-flxbZVlAofzwm3E0 .node .label,#mermaid-svg-flxbZVlAofzwm3E0 .image-shape .label,#mermaid-svg-flxbZVlAofzwm3E0 .icon-shape .label{text-align:center;}#mermaid-svg-flxbZVlAofzwm3E0 .node.clickable{cursor:pointer;}#mermaid-svg-flxbZVlAofzwm3E0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-flxbZVlAofzwm3E0 .arrowheadPath{fill:#333333;}#mermaid-svg-flxbZVlAofzwm3E0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-flxbZVlAofzwm3E0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-flxbZVlAofzwm3E0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-flxbZVlAofzwm3E0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-flxbZVlAofzwm3E0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-flxbZVlAofzwm3E0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-flxbZVlAofzwm3E0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-flxbZVlAofzwm3E0 .cluster text{fill:#333;}#mermaid-svg-flxbZVlAofzwm3E0 .cluster span{color:#333;}#mermaid-svg-flxbZVlAofzwm3E0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-flxbZVlAofzwm3E0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-flxbZVlAofzwm3E0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-flxbZVlAofzwm3E0 .icon-shape,#mermaid-svg-flxbZVlAofzwm3E0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-flxbZVlAofzwm3E0 .icon-shape p,#mermaid-svg-flxbZVlAofzwm3E0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-flxbZVlAofzwm3E0 .icon-shape rect,#mermaid-svg-flxbZVlAofzwm3E0 .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-flxbZVlAofzwm3E0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-flxbZVlAofzwm3E0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-flxbZVlAofzwm3E0 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Keycloak issues JWT
Header + payload signed
Keycloak private key
JWT returned to alice
Service decodes payload to read claims
banking-api-service validates signature via JWKS
Kong introspects token with Keycloak
Claims can be trusted
关键观念是:解码与信任不是一回事。
- 解码回答:令牌说了什么?
- 校验回答:它真的由 Keycloak 签发吗?它现在可接受吗?
- 内省回答:Keycloak 是否仍认为该令牌处于 active?
签名验证如何工作
JWT 有三部分:
header.payload.signature
Keycloak 用其私钥对 header.payload 部分签名。 签名作为第三段附加在后面。
当服务校验令牌时,它做相反的过程:
若二者匹配,则令牌是用配对的私钥签名的,且 payload 未被改动。 若不匹配,则令牌被拒绝。
私钥与公钥
Keycloak 把私钥保密。
服务从不需要私钥。 它们只需要公钥,而公钥是可以安全共享的。
正是这把公钥,让 banking-api-service 与其他服务能够验证某个令牌来自期望的 Keycloak realm。
本 PoC 中的密码学机制
本 PoC 中的访问令牌使用 RS256:
- R = RSA 公钥密码学
- S = 签名(signature)
- 256 = SHA-256 哈希
签名过程:
当服务校验令牌时,它反过来:
若签名是用配对的私钥创建的,验证成功。 若令牌被修改过,验证失败,因为哈希不再与被签名的内容匹配。
用大白话说:
- Keycloak 用其私钥签名
- 服务用 Keycloak 的公钥验证
- SHA-256 哈希让 payload 一旦被篡改即可被发现
服务如何在 JWKS 端点发现并缓存 Keycloak 的公钥,见 12 — JWKS 深入解析。
校验通常检查什么
校验比签名检查更宽泛。
服务通常检查:
- 签名有效
- exp 未过
- iss 与期望的 realm 匹配
- aud 与期望的 client 或 API 匹配
- 令牌类型符合服务的预期
若其中任一检查失败,该令牌不应被信任用于授权决策。
在本 PoC 中,banking-api-service 对传入请求执行 JWT 校验,因此它能拒绝过期的、格式错误的、或为错误受众签发的令牌。
内省实际检查什么
本节是本系列文档中对内省机制的唯一完整论述。
内省时 Keycloak 做什么
当 Kong 调用 Keycloak 的内省端点时,Keycloak 做的不止是解码 JWT。
在高层面上,Keycloak 检查:
若这些检查通过,Keycloak 返回:
{
"active": true
}
若不通过,Keycloak 返回:
{
"active": false
}
所以内省回答的是:Keycloak 此刻是否仍认为该令牌可用?
为什么即便对结构合法、未过期的 JWT,Keycloak 也可能返回 active: false
这是整个技术栈中最重要的观念之一。
因为 Keycloak 保有服务端会话状态,所以即便满足以下条件,它也能说一个令牌是 inactive:
- 令牌看起来仍是一个有效的 JWT
- 声明仍能被解码
- 令牌尚未到达其 exp 时间戳
原因在于:Keycloak 不仅是该 JWT 的签发者 —— 它还是该 JWT 背后实时会话状态的拥有者。
Keycloak 可能返回 active: false 的例子:
- alice 登出了
- 用户会话过期了
- 客户端会话过期了
- alice 账户被禁用了
- 该 client(如 mobile-banking-app)被禁用了
- 发生了 realm 或 client 的失效事件
所以当 Keycloak 收到一个内省请求时,它不仅在问「这个 JWT 看起来格式良好吗?」—— 它还在问「依据 Keycloak 此刻的状态,这个令牌是否仍属于一个存活且可接受的会话?」
JWT 携带令牌数据。Keycloak 保有会话真相。
这是关于内省最重要的思维模型。
Keycloak 如何保有会话状态
访问令牌与会话相关,但不是同一回事。
- JWT 携带身份与声明数据
- Keycloak 在服务端保有实时会话信息
Keycloak 通常维护以下会话层:
| 认证会话 | 登录流程期间的临时状态;登录完成或过期后移除 |
| 用户会话 | 某个 realm 中被认证的用户会话 —— 跟踪开始时间、空闲/过期、登出状态 |
| 客户端会话 | 该用户会话中按 client 划分的参与(例如对 mobile-banking-app) |
运行时,Keycloak 主要把在线会话状态存放在 Infinispan 缓存中。离线会话则持久化在数据库中。在集群部署里,缓存在各节点间复制。
实践要点:令牌声明随 JWT 携带,但实时会话活动维护在服务端。这正是为什么一个令牌可以被正确解码,但若其背后的会话状态已不存在,内省仍可能返回 active: false。
本 PoC 中的内省流程
banking-api-service
OPA
Keycloak
Kong
alice
banking-api-service
OPA
Keycloak
Kong
alice
#mermaid-svg-45jclNgOWEjvmk8u{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-45jclNgOWEjvmk8u .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-45jclNgOWEjvmk8u .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-45jclNgOWEjvmk8u .error-icon{fill:#552222;}#mermaid-svg-45jclNgOWEjvmk8u .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-45jclNgOWEjvmk8u .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-45jclNgOWEjvmk8u .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-45jclNgOWEjvmk8u .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-45jclNgOWEjvmk8u .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-45jclNgOWEjvmk8u .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-45jclNgOWEjvmk8u .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-45jclNgOWEjvmk8u .marker{fill:#333333;stroke:#333333;}#mermaid-svg-45jclNgOWEjvmk8u .marker.cross{stroke:#333333;}#mermaid-svg-45jclNgOWEjvmk8u svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-45jclNgOWEjvmk8u p{margin:0;}#mermaid-svg-45jclNgOWEjvmk8u .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-45jclNgOWEjvmk8u text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-45jclNgOWEjvmk8u .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-45jclNgOWEjvmk8u .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-45jclNgOWEjvmk8u .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-45jclNgOWEjvmk8u .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-45jclNgOWEjvmk8u #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-45jclNgOWEjvmk8u .sequenceNumber{fill:white;}#mermaid-svg-45jclNgOWEjvmk8u #sequencenumber{fill:#333;}#mermaid-svg-45jclNgOWEjvmk8u #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-45jclNgOWEjvmk8u .messageText{fill:#333;stroke:none;}#mermaid-svg-45jclNgOWEjvmk8u .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-45jclNgOWEjvmk8u .labelText,#mermaid-svg-45jclNgOWEjvmk8u .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-45jclNgOWEjvmk8u .loopText,#mermaid-svg-45jclNgOWEjvmk8u .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-45jclNgOWEjvmk8u .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-45jclNgOWEjvmk8u .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-45jclNgOWEjvmk8u .noteText,#mermaid-svg-45jclNgOWEjvmk8u .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-45jclNgOWEjvmk8u .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-45jclNgOWEjvmk8u .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-45jclNgOWEjvmk8u .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-45jclNgOWEjvmk8u .actorPopupMenu{position:absolute;}#mermaid-svg-45jclNgOWEjvmk8u .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-45jclNgOWEjvmk8u .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-45jclNgOWEjvmk8u .actor-man circle,#mermaid-svg-45jclNgOWEjvmk8u line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-45jclNgOWEjvmk8u :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
API request + bearer token
POST /introspect (token)
check cryptography + session state
active: true or active: false
policy input (claims from token)
allow or deny
forward allowed request
validate JWT signature + issuer + audience
API response
会话与令牌的关系
#mermaid-svg-4Xc1P54YRMEA2zxR{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4Xc1P54YRMEA2zxR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4Xc1P54YRMEA2zxR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4Xc1P54YRMEA2zxR .error-icon{fill:#552222;}#mermaid-svg-4Xc1P54YRMEA2zxR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4Xc1P54YRMEA2zxR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4Xc1P54YRMEA2zxR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4Xc1P54YRMEA2zxR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4Xc1P54YRMEA2zxR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4Xc1P54YRMEA2zxR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4Xc1P54YRMEA2zxR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4Xc1P54YRMEA2zxR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4Xc1P54YRMEA2zxR .marker.cross{stroke:#333333;}#mermaid-svg-4Xc1P54YRMEA2zxR svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4Xc1P54YRMEA2zxR p{margin:0;}#mermaid-svg-4Xc1P54YRMEA2zxR .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-4Xc1P54YRMEA2zxR .cluster-label text{fill:#333;}#mermaid-svg-4Xc1P54YRMEA2zxR .cluster-label span{color:#333;}#mermaid-svg-4Xc1P54YRMEA2zxR .cluster-label span p{background-color:transparent;}#mermaid-svg-4Xc1P54YRMEA2zxR .label text,#mermaid-svg-4Xc1P54YRMEA2zxR span{fill:#333;color:#333;}#mermaid-svg-4Xc1P54YRMEA2zxR .node rect,#mermaid-svg-4Xc1P54YRMEA2zxR .node circle,#mermaid-svg-4Xc1P54YRMEA2zxR .node ellipse,#mermaid-svg-4Xc1P54YRMEA2zxR .node polygon,#mermaid-svg-4Xc1P54YRMEA2zxR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4Xc1P54YRMEA2zxR .rough-node .label text,#mermaid-svg-4Xc1P54YRMEA2zxR .node .label text,#mermaid-svg-4Xc1P54YRMEA2zxR .image-shape .label,#mermaid-svg-4Xc1P54YRMEA2zxR .icon-shape .label{text-anchor:middle;}#mermaid-svg-4Xc1P54YRMEA2zxR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4Xc1P54YRMEA2zxR .rough-node .label,#mermaid-svg-4Xc1P54YRMEA2zxR .node .label,#mermaid-svg-4Xc1P54YRMEA2zxR .image-shape .label,#mermaid-svg-4Xc1P54YRMEA2zxR .icon-shape .label{text-align:center;}#mermaid-svg-4Xc1P54YRMEA2zxR .node.clickable{cursor:pointer;}#mermaid-svg-4Xc1P54YRMEA2zxR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4Xc1P54YRMEA2zxR .arrowheadPath{fill:#333333;}#mermaid-svg-4Xc1P54YRMEA2zxR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4Xc1P54YRMEA2zxR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4Xc1P54YRMEA2zxR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4Xc1P54YRMEA2zxR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4Xc1P54YRMEA2zxR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4Xc1P54YRMEA2zxR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4Xc1P54YRMEA2zxR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4Xc1P54YRMEA2zxR .cluster text{fill:#333;}#mermaid-svg-4Xc1P54YRMEA2zxR .cluster span{color:#333;}#mermaid-svg-4Xc1P54YRMEA2zxR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4Xc1P54YRMEA2zxR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4Xc1P54YRMEA2zxR rect.text{fill:none;stroke-width:0;}#mermaid-svg-4Xc1P54YRMEA2zxR .icon-shape,#mermaid-svg-4Xc1P54YRMEA2zxR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4Xc1P54YRMEA2zxR .icon-shape p,#mermaid-svg-4Xc1P54YRMEA2zxR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4Xc1P54YRMEA2zxR .icon-shape rect,#mermaid-svg-4Xc1P54YRMEA2zxR .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4Xc1P54YRMEA2zxR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4Xc1P54YRMEA2zxR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4Xc1P54YRMEA2zxR :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
alice login
Keycloak
Server-side session state
Access token and refresh token
Kong introspection
banking-api-service JWT validation
JWKS vs 内省 —— 该用哪个
本 PoC 两种机制都用,但用在不同的地方。
为什么 banking-api-service 用 JWKS 而非内省
banking-api-service 充当资源服务器。它用 JWKS 做 JWT 校验,理由如下:
1. 本地且快速。 banking-api-service 从 JWKS 端点下载 Keycloak 的公钥后,便能在本地校验 JWT,每个请求都无需网络往返。
2. JWT 本就为此模式而设计。 一个自包含的 JWT 已经携带了声明、过期、签发者、受众与签名。这使得本地 JWKS 校验成为 Spring Security JWT 资源服务器自然且标准的模式。
3. 更好的韧性。 若 Keycloak 短暂变慢或不可用,只要服务已经拥有所需公钥,对已签发的令牌做本地 JWT 校验仍可工作。每个请求都内省会对 Keycloak 的实时可用性形成硬依赖。
4. 更清晰的资源服务器边界。 Spring Security 的资源服务器支持,正是为「bearer JWT + 通过 JWKS 发现公钥 + 签发者与受众校验」而设计的。使用 JWKS 是标准、高效的模型 —— 而非权宜之计。
密钥轮换机制与 JWKS 缓存见 12 — JWKS 深入解析。
每种机制回答什么
| JWKS 校验 | 该令牌是否由受信任的签发者签名?它是否有正确的 iss、aud 与 exp? |
| 内省 | Keycloak 此刻是否仍认为该令牌处于 active? |
JWKS 校验回答令牌在密码学上是否可信。 内省再补上一个实时答案:令牌背后的会话是否仍存活。
为什么本 PoC 两者都用
| Kong | 内省 | 边缘的实时令牌活跃性检查 —— 在构建 OPA 策略 input 之前,向 Keycloak 询问令牌是否 active |
| banking-api-service | JWKS 校验 | 本地密码学验证 —— 服务不盲目相信网关已经检查了一切 |
纵深防御:即便 Kong 已经内省过,banking-api-service 仍把 JWT 校验作为自己的信任边界。
简短版:
- 内省 = 实时会话状态检查
- JWKS 校验 = 本地密码学信任检查
声明用来做什么
声明携带身份与授权上下文。
示例:
- iss 告诉服务令牌由谁签发
- aud 告诉服务令牌是发给哪个 client 或 API 的
- preferred_username 给出可读的用户名(alice、ops-admin)
- realm_access.roles 给出角色信息
- customer_id 与 account_ids 携带业务上下文
声明一经解码就很容易读取,但只有在校验或内省之后,才可以安全地据其行动。
端到端示例(alice)
两层信任:
- 实时的 Keycloak 会话验证(经由 Kong 内省)
- 本地的密码学验证(经由 banking-api-service 的 JWKS 校验)
关于访问令牌如何通过刷新令牌续期,见 13 — 访问令牌与刷新令牌的生命周期。
小结
- 解码(decode) = 读出令牌说了什么
- 校验(validate) = 证明令牌是真实的、是发给本服务的、且未过期
- 内省(introspect) = 询问 Keycloak 令牌的会话是否仍存活
签名让令牌一旦被篡改即可被发现。 校验让令牌在本地可被接受。 内省给出来自 Keycloak 的实时答案 —— 而 Keycloak 即便对一个结构合法、未过期的 JWT 也能返回 active: false,因为它拥有的是会话真相,而不仅仅是令牌字符串。
← Prev: 10 — identity-bootstrap-service · Next: 12 — JWKS 深入解析 →



