微服务认证与授权:06 — Keycloak / IdP

📦 GitHub: https://github.com/geekchow/micro-service-auth

Keycloak 是本 PoC 的身份提供方(Identity Provider):它对用户进行认证、拥有会话状态,并签发驱动一切下游授权决策的 JWT。

什么是 IdP

IdP(身份提供方)是负责认证用户并签发令牌的系统。完整术语表见 01 --- 概念

简而言之:

  • IdP 证明用户是谁
  • 其他系统决定该用户能做什么

本项目为什么需要 IdP

如果没有 IdP,每个服务都得各自实现并维护自己的登录逻辑、令牌签发逻辑、密码处理、用户存储与声明管理。

那会造成:

  • 重复的安全代码
  • 各服务之间不一致的身份规则
  • 更难的审计与排查
  • 更难与网关、策略引擎集成

本项目使用 Keycloak 作为 IdP,从而把身份集中化:

  • 一个统一的地方认证用户
  • 一个统一的地方管理像 customerops-admin 这样的角色
  • 一个统一的地方签发 JWT
  • 一个统一的地方定义像 customer_idaccount_ids 这样的声明

Keycloak 在本 PoC 中的位置

#mermaid-svg-wrqWQkW0OT8AYL8P{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-wrqWQkW0OT8AYL8P .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wrqWQkW0OT8AYL8P .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wrqWQkW0OT8AYL8P .error-icon{fill:#552222;}#mermaid-svg-wrqWQkW0OT8AYL8P .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wrqWQkW0OT8AYL8P .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wrqWQkW0OT8AYL8P .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wrqWQkW0OT8AYL8P .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wrqWQkW0OT8AYL8P .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wrqWQkW0OT8AYL8P .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wrqWQkW0OT8AYL8P .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wrqWQkW0OT8AYL8P .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wrqWQkW0OT8AYL8P .marker.cross{stroke:#333333;}#mermaid-svg-wrqWQkW0OT8AYL8P svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wrqWQkW0OT8AYL8P p{margin:0;}#mermaid-svg-wrqWQkW0OT8AYL8P .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wrqWQkW0OT8AYL8P .cluster-label text{fill:#333;}#mermaid-svg-wrqWQkW0OT8AYL8P .cluster-label span{color:#333;}#mermaid-svg-wrqWQkW0OT8AYL8P .cluster-label span p{background-color:transparent;}#mermaid-svg-wrqWQkW0OT8AYL8P .label text,#mermaid-svg-wrqWQkW0OT8AYL8P span{fill:#333;color:#333;}#mermaid-svg-wrqWQkW0OT8AYL8P .node rect,#mermaid-svg-wrqWQkW0OT8AYL8P .node circle,#mermaid-svg-wrqWQkW0OT8AYL8P .node ellipse,#mermaid-svg-wrqWQkW0OT8AYL8P .node polygon,#mermaid-svg-wrqWQkW0OT8AYL8P .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wrqWQkW0OT8AYL8P .rough-node .label text,#mermaid-svg-wrqWQkW0OT8AYL8P .node .label text,#mermaid-svg-wrqWQkW0OT8AYL8P .image-shape .label,#mermaid-svg-wrqWQkW0OT8AYL8P .icon-shape .label{text-anchor:middle;}#mermaid-svg-wrqWQkW0OT8AYL8P .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wrqWQkW0OT8AYL8P .rough-node .label,#mermaid-svg-wrqWQkW0OT8AYL8P .node .label,#mermaid-svg-wrqWQkW0OT8AYL8P .image-shape .label,#mermaid-svg-wrqWQkW0OT8AYL8P .icon-shape .label{text-align:center;}#mermaid-svg-wrqWQkW0OT8AYL8P .node.clickable{cursor:pointer;}#mermaid-svg-wrqWQkW0OT8AYL8P .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wrqWQkW0OT8AYL8P .arrowheadPath{fill:#333333;}#mermaid-svg-wrqWQkW0OT8AYL8P .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wrqWQkW0OT8AYL8P .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wrqWQkW0OT8AYL8P .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wrqWQkW0OT8AYL8P .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wrqWQkW0OT8AYL8P .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wrqWQkW0OT8AYL8P .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wrqWQkW0OT8AYL8P .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wrqWQkW0OT8AYL8P .cluster text{fill:#333;}#mermaid-svg-wrqWQkW0OT8AYL8P .cluster span{color:#333;}#mermaid-svg-wrqWQkW0OT8AYL8P 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-wrqWQkW0OT8AYL8P .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wrqWQkW0OT8AYL8P rect.text{fill:none;stroke-width:0;}#mermaid-svg-wrqWQkW0OT8AYL8P .icon-shape,#mermaid-svg-wrqWQkW0OT8AYL8P .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wrqWQkW0OT8AYL8P .icon-shape p,#mermaid-svg-wrqWQkW0OT8AYL8P .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wrqWQkW0OT8AYL8P .icon-shape .label rect,#mermaid-svg-wrqWQkW0OT8AYL8P .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wrqWQkW0OT8AYL8P .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wrqWQkW0OT8AYL8P .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wrqWQkW0OT8AYL8P :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} User or demo script
Keycloak
JWT access token
Kong
OPA
banking-api-service
identity-bootstrap-service

Keycloak 不是网关、不是策略引擎、也不是银行 API。它是身份与令牌声明的来源。

Keycloak 如何工作

Realm

realm 是一个安全边界,拥有自己的用户、client、角色与令牌设置。

本项目使用 realm banking-poc。所有演示用户与 client 都位于该 realm 内。

用户

用户是用于登录的身份。本 PoC 中的演示用户是:

  • alice
  • ops-admin

identity-bootstrap-service 通过 Keycloak 管理 API 创建并管理这些用户,设置如下属性:

  • customer_id
  • account_ids
  • demo_managed

角色

角色代表粗粒度的权限分组。本 PoC 使用两个 realm 角色:

  • customer
  • ops-admin

这些角色出现在 JWT 中,被 KongOPAbanking-api-service 使用。

Client

client 是与 Keycloak 交互的应用或服务。本仓库定义了两个:

  1. mobile-banking-app ------ 公共 client,演示登录流程用它来获取访问令牌(directAccessGrantsEnabled: truepublicClient: true
  2. kong-introspection ------ 机密的服务账户 client(publicClient: falseserviceAccountsEnabled: true),由 Kong 用来内省令牌

协议映射器(Protocol Mappers)

协议映射器控制哪些声明进入令牌。mobile-banking-app 映射了:

  • customer_id ------ 来自用户属性,写入访问令牌、ID 令牌与 userinfo
  • account_ids ------ 来自用户属性(多值),写入访问令牌、ID 令牌与 userinfo
  • 受众 mobile-banking-app ------ 仅写入访问令牌

这些声明会流向下游:Kong 把它们转发给 OPAOPA 在策略中使用它们,banking-api-service 用它们做纵深防御检查。

令牌

登录成功后,Keycloak 签发一个签名的 JWT 访问令牌,包含:

  • sub
  • preferred_username
  • aud
  • iss
  • exp
  • realm_access.roles
  • customer_id
  • account_ids

该令牌由 Keycloak 签名,从而让下游系统能够以密码学方式校验它。

本项目中的 OAuth 2.0 与 OpenID Connect

Keycloak 讲的是标准的身份协议。

OAuth 2.0

OAuth 2.0 是一套用于委托访问的框架。它提供了获取访问令牌、并在调用 API 时使用这些令牌的标准方式。

OpenID Connect

OpenID Connect 是建立在 OAuth 2.0 之上的身份层。它标准化了身份声明与端点。

实践中:

  • Keycloak 认证用户
  • Keycloak 签发兼容 OIDC 的 JWT
  • 服务用这些令牌来识别调用方

这里使用的流程

演示脚本使用直接的用户名/密码令牌交换(directAccessGrantsEnabled)来对接 Keycloak。

对本 PoC 而言重要的是:

  1. 凭据发送给 Keycloak
  2. Keycloak 校验凭据
  3. Keycloak 返回一个签名的 JWT 访问令牌
  4. 该令牌被发送到 Kong 以及银行 API 路径上

Keycloak 的会话与令牌活跃性

Keycloak 拥有 JWT 背后的实时会话状态 ------ 这正是为什么即便对一个结构上合法的 JWT,令牌内省也可能回答 active: false。完整的内省机制见 11 --- JWT 签名、校验与内省

逻辑会话类型

Keycloak 跟踪三层会话状态:

  1. 认证会话(authentication session,短暂) ------ 登录流程期间的临时状态;登录完成或过期后移除
  2. 用户会话(user session) ------ 表示某个 realm 中被认证的用户;跟踪开始时间、空闲/过期状态与登出状态
  3. 客户端会话(client session,按 client 划分) ------ 为每个 client(如 mobile-banking-app)附着在某个用户会话上;跟踪该 client 在此次登录会话中的参与情况

物理存储

运行时,Keycloak 把在线会话状态存放在 Infinispan 缓存中。在集群部署里,这些缓存分布在各节点上。离线会话则持久化在数据库中。

关键的实践要点:令牌声明随 JWT 一同传输,但实时会话状态存放在 Keycloak 的服务端。这正是为什么一个令牌可以被正确解码,而内省仍可能返回 active: false

关于访问令牌/刷新令牌的生命周期、以及它们与会话状态的关系,详见 13 --- 访问令牌与刷新令牌的生命周期

令牌签发与校验流程

banking-api-service Kong Keycloak User banking-api-service Kong Keycloak User #mermaid-svg-XlRlhQi5C3zBRQvj{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-XlRlhQi5C3zBRQvj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XlRlhQi5C3zBRQvj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XlRlhQi5C3zBRQvj .error-icon{fill:#552222;}#mermaid-svg-XlRlhQi5C3zBRQvj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XlRlhQi5C3zBRQvj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XlRlhQi5C3zBRQvj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XlRlhQi5C3zBRQvj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XlRlhQi5C3zBRQvj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XlRlhQi5C3zBRQvj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XlRlhQi5C3zBRQvj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XlRlhQi5C3zBRQvj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XlRlhQi5C3zBRQvj .marker.cross{stroke:#333333;}#mermaid-svg-XlRlhQi5C3zBRQvj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XlRlhQi5C3zBRQvj p{margin:0;}#mermaid-svg-XlRlhQi5C3zBRQvj .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-XlRlhQi5C3zBRQvj text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-XlRlhQi5C3zBRQvj .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-XlRlhQi5C3zBRQvj .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-XlRlhQi5C3zBRQvj .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-XlRlhQi5C3zBRQvj .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-XlRlhQi5C3zBRQvj #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-XlRlhQi5C3zBRQvj .sequenceNumber{fill:white;}#mermaid-svg-XlRlhQi5C3zBRQvj #sequencenumber{fill:#333;}#mermaid-svg-XlRlhQi5C3zBRQvj #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-XlRlhQi5C3zBRQvj .messageText{fill:#333;stroke:none;}#mermaid-svg-XlRlhQi5C3zBRQvj .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-XlRlhQi5C3zBRQvj .labelText,#mermaid-svg-XlRlhQi5C3zBRQvj .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-XlRlhQi5C3zBRQvj .loopText,#mermaid-svg-XlRlhQi5C3zBRQvj .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-XlRlhQi5C3zBRQvj .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-XlRlhQi5C3zBRQvj .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-XlRlhQi5C3zBRQvj .noteText,#mermaid-svg-XlRlhQi5C3zBRQvj .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-XlRlhQi5C3zBRQvj .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-XlRlhQi5C3zBRQvj .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-XlRlhQi5C3zBRQvj .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-XlRlhQi5C3zBRQvj .actorPopupMenu{position:absolute;}#mermaid-svg-XlRlhQi5C3zBRQvj .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-XlRlhQi5C3zBRQvj .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-XlRlhQi5C3zBRQvj .actor-man circle,#mermaid-svg-XlRlhQi5C3zBRQvj line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-XlRlhQi5C3zBRQvj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} username + password authenticate user signed JWT access token API request + bearer token introspect token active or inactive forward allowed request fetch JWKS (for signature validation) validate issuer + audience + signature + expiry API response

为什么登录之后仍然需要校验 JWT

一个常见的误解:

"Keycloak 已经认证过用户了,所以服务不需要再次校验令牌。"

这是错的。一旦令牌离开 Keycloak,下游系统仍必须验证:该令牌确实由 Keycloak 签发、是发给本应用的、尚未过期、且未被篡改。

这正是为什么本 PoC 在多个地方校验令牌:

  • Kong 用 Keycloak 做内省
  • banking-api-service 校验 JWT 的签名、签发者与受众

Keycloak 在本仓库中如何配置

主配置文件是 infra/keycloak/realm-export.json

从该文件核实的关键设置:

设置
Realm banking-poc
Realm 角色 customerops-admin
公共 client mobile-banking-appdirectAccessGrantsEnabled: true
机密 client kong-introspectionserviceAccountsEnabled: true
协议映射器 customer_idaccount_ids(用户属性),aud=mobile-banking-app
用户档案属性 customer_id(单值)、account_ids(多值)------ 仅管理员可编辑

realm-export 还声明了用户档案(user-profile)schema,以便 identity-bootstrap-service 能安全地写入 customer_idaccount_ids 属性。

Keycloak 如何与其他组件协作

Keycloak 与 identity-bootstrap-service

bootstrap 服务使用 Keycloak 管理 API 来:

  • 创建演示用户
  • 设置密码
  • 分配 realm 角色
  • 设置自定义属性(customer_idaccount_idsdemo_managed

这让 PoC 无需手动配置 Keycloak 即可重复运行。

Keycloak 与 Kong

Kong 在转发请求或询问 OPA 决策之前,使用 Keycloak 令牌内省来检查令牌是否处于 active。Kong 使用 kong-introspection 机密 client 的凭据向 Keycloak 认证。

Keycloak 与 OPA

OPA 不直接与 Keycloak 通信。取而代之的是:

  • Keycloak 在 JWT 中签发声明
  • Kong 在内省后读取经校验的令牌上下文
  • Kong 把相关声明发给 OPA
  • OPA 在策略中使用这些声明

所以 Keycloak 是通过令牌声明间接影响 OPA 的决策。

Keycloak 与 banking-api-service

banking-api-service 信任 Keycloak 作为令牌签发者,但会独立校验:

  • 签名(通过 JWKS)
  • 签发者
  • 受众

然后读取诸如 realm_access.rolescustomer_idaccount_ids 之类的声明,用于服务端的授权检查。

本 PoC 中的声明流转

#mermaid-svg-3MjJQK8lUjkYUNAq{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-3MjJQK8lUjkYUNAq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3MjJQK8lUjkYUNAq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3MjJQK8lUjkYUNAq .error-icon{fill:#552222;}#mermaid-svg-3MjJQK8lUjkYUNAq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3MjJQK8lUjkYUNAq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3MjJQK8lUjkYUNAq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3MjJQK8lUjkYUNAq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3MjJQK8lUjkYUNAq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3MjJQK8lUjkYUNAq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3MjJQK8lUjkYUNAq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3MjJQK8lUjkYUNAq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3MjJQK8lUjkYUNAq .marker.cross{stroke:#333333;}#mermaid-svg-3MjJQK8lUjkYUNAq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3MjJQK8lUjkYUNAq p{margin:0;}#mermaid-svg-3MjJQK8lUjkYUNAq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3MjJQK8lUjkYUNAq .cluster-label text{fill:#333;}#mermaid-svg-3MjJQK8lUjkYUNAq .cluster-label span{color:#333;}#mermaid-svg-3MjJQK8lUjkYUNAq .cluster-label span p{background-color:transparent;}#mermaid-svg-3MjJQK8lUjkYUNAq .label text,#mermaid-svg-3MjJQK8lUjkYUNAq span{fill:#333;color:#333;}#mermaid-svg-3MjJQK8lUjkYUNAq .node rect,#mermaid-svg-3MjJQK8lUjkYUNAq .node circle,#mermaid-svg-3MjJQK8lUjkYUNAq .node ellipse,#mermaid-svg-3MjJQK8lUjkYUNAq .node polygon,#mermaid-svg-3MjJQK8lUjkYUNAq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3MjJQK8lUjkYUNAq .rough-node .label text,#mermaid-svg-3MjJQK8lUjkYUNAq .node .label text,#mermaid-svg-3MjJQK8lUjkYUNAq .image-shape .label,#mermaid-svg-3MjJQK8lUjkYUNAq .icon-shape .label{text-anchor:middle;}#mermaid-svg-3MjJQK8lUjkYUNAq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3MjJQK8lUjkYUNAq .rough-node .label,#mermaid-svg-3MjJQK8lUjkYUNAq .node .label,#mermaid-svg-3MjJQK8lUjkYUNAq .image-shape .label,#mermaid-svg-3MjJQK8lUjkYUNAq .icon-shape .label{text-align:center;}#mermaid-svg-3MjJQK8lUjkYUNAq .node.clickable{cursor:pointer;}#mermaid-svg-3MjJQK8lUjkYUNAq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3MjJQK8lUjkYUNAq .arrowheadPath{fill:#333333;}#mermaid-svg-3MjJQK8lUjkYUNAq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3MjJQK8lUjkYUNAq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3MjJQK8lUjkYUNAq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3MjJQK8lUjkYUNAq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3MjJQK8lUjkYUNAq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3MjJQK8lUjkYUNAq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3MjJQK8lUjkYUNAq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3MjJQK8lUjkYUNAq .cluster text{fill:#333;}#mermaid-svg-3MjJQK8lUjkYUNAq .cluster span{color:#333;}#mermaid-svg-3MjJQK8lUjkYUNAq 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-3MjJQK8lUjkYUNAq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3MjJQK8lUjkYUNAq rect.text{fill:none;stroke-width:0;}#mermaid-svg-3MjJQK8lUjkYUNAq .icon-shape,#mermaid-svg-3MjJQK8lUjkYUNAq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3MjJQK8lUjkYUNAq .icon-shape p,#mermaid-svg-3MjJQK8lUjkYUNAq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3MjJQK8lUjkYUNAq .icon-shape .label rect,#mermaid-svg-3MjJQK8lUjkYUNAq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3MjJQK8lUjkYUNAq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3MjJQK8lUjkYUNAq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3MjJQK8lUjkYUNAq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} issues JWT with roles customer_id account_ids
introspect token
send claims to OPA
allow or deny
forward request
validate JWT again
use claims for defense in depth
Keycloak
Kong
OPA
banking-api-service
Response

本 PoC 中的实际示例

示例 1:alice 访问她自己的账户

Keycloak 为 alice 签发一个令牌,包含:

  • 角色 customer
  • customer_id=C-1001
  • account_ids=[A-1001]

随后:

  • Kong 确认令牌处于 active
  • OPA 看到 A-1001 在令牌声明中
  • banking-api-service 再次检查同一组声明
  • 请求被允许

示例 2:alice 访问另一个账户

如果 alice 请求账户 A-2001

  • 该令牌仍然是一个有效的身份令牌
  • 但声明并不授权访问 A-2001
  • OPA 返回 deny
  • Kong 返回 403

这体现了关键思想:有效的身份并不自动意味着有效的授权。

示例 3:ops-admin 的访问

如果用户具备 ops-admin 角色:

  • OPA 允许更宽的访问
  • banking-api-service 也看到该角色并在服务端允许访问

为什么 Keycloak 在这里很合适

Keycloak 提供了:

  • 集中化的认证
  • 标准的令牌签发(OIDC/OAuth 2.0)
  • 通过协议映射器支持自定义声明
  • 用于自动化演示初始化的管理 API
  • 面向网关与服务的标准集成模式

它让项目能够专注于银行授权逻辑,而不必重新发明登录基础设施。

常见误解

"Keycloak 已经处理了所有安全"

不。Keycloak 处理身份与令牌签发。它不替代网关执行(Kong)、策略评估(OPA)或业务服务检查(banking-api-service)。

"OPA 可以替代 Keycloak"

不。OPAPDP ------ 它判定策略。它不认证用户、也不签发令牌。

"Kong 可以替代 Keycloak"

不。KongPEP ------ 网关。它不是身份提供方。

"Spring Boot 自己全干了不就行"

技术上它能做更多,但那样会把身份、策略与业务逻辑塌缩到一个地方,使架构更难维护、更难理解。

小结

  1. Keycloak 认证用户
  2. Keycloak 签发携带身份与权益声明的 JWT
  3. Kong 检查令牌活跃性,并向 OPA 请求授权决策
  4. banking-api-service 再次校验 JWT 并执行服务端检查

这就是 Keycloak 在本项目中的意义:它是那个以标准、集中、可重复的方式让其余安全流程得以成立的系统。


← Prev: 05 --- 组件巡览 · Next: 07 --- Kong


📚 返回专栏目录

相关推荐
2601_962851741 小时前
计算机毕业设计之基于YOLOV10的低光照目标检测算法研究与实现
大数据·算法·yolo·目标检测·信息可视化·cnn·课程设计
BreezeJiang1 小时前
为什么你调的 LLM 接口不值钱?——从零理解 Agent 开发的正确姿势
人工智能·架构
多年小白2 小时前
全球 AI 行业近三年市场增长数据深度分析报告(2023-2026)
人工智能·信息可视化
李燚2 小时前
五个适配器:DeepFlux 如何把 Eino 接进 DDD 架构
架构·ddd·eino·deepflux
我叫张小白。2 小时前
一个微服务电商+社区项目(瓷韵app)的技术深度复盘
docker·微服务·云原生·架构
_abab2 小时前
Java面试宝典:从基础到架构2
java·面试·架构
会周易的程序员3 小时前
microLog 的 log_reader 架构解析:嵌入式日志检索引擎的设计之道
c++·物联网·架构·日志·iot·aiot
Geek-Chow3 小时前
微服务认证与授权:08 — OPA(PDP)
微服务·云原生·架构
阿标在干嘛3 小时前
100+微服务的统一出入口:政策快报平台的API网关设计
微服务·云原生·架构