微服务认证与授权:07 — Kong(网关 / PEP)
07 — Kong(网关 / PEP)
📦 GitHub: https://github.com/geekchow/micro-service-auth
Kong 是 API 网关,也是策略执行点(PEP):它拦截每个请求、用Keycloak校验令牌、向OPA请求授权决策,并把被允许的请求转发给banking-api-service。
什么是 Kong
在本 PoC 所用的 IdP / PEP / PDP 分工中:
Keycloak是IdP—— 它签发令牌。Kong是PEP—— 它在边缘执行访问控制。OPA是PDP—— 它判定某个请求是否被允许。banking-api-service是资源服务器—— 它拥有受保护的银行数据。
Kong 在本项目中的职责:
- 接收传入的 API 请求。
- 拒绝缺失或格式错误的
Authorization头的请求。 - 调用
Keycloak内省,确认令牌当前处于 active。 - 从令牌载荷中解码 JWT 声明。
- 用请求路径、方法与声明构建一个 input 对象。
- 调用
OPA获取授权决策。 - 若
OPA拒绝则返回403,若OPA允许则转发给banking-api-service。
Kong 不是身份提供方、不是策略引擎、也不是银行业务服务。
两个与 Kong 相关的配置文件
本仓库中有两个文件控制 Kong:
| 文件 | 用途 |
|---|---|
docker-compose.yml | 容器串联 —— 镜像、端口、环境变量、卷、依赖 |
infra/kong/kong.yml | Kong 运行时行为 —— service、route、plugin、plugin 配置值 |
它们之间的关系
docker-compose.yml创建 Kong 容器并把配置挂载进去。kong.yml告诉 Kong 要暴露什么路由、运行什么插件。schema.lua定义哪些插件配置字段是合法的。handler.lua包含每个请求都会运行的执行逻辑。
docker-compose.yml中的 Kong
相关的 Kong service 块:
kong:image:kong:3.7environment:KONG_DATABASE:offKONG_DECLARATIVE_CONFIG:/etc/kong/kong.ymlKONG_PLUGINS:bundled,opa-authzKONG_PROXY_ACCESS_LOG:/dev/stdoutKONG_ADMIN_ACCESS_LOG:/dev/stdoutKONG_PROXY_ERROR_LOG:/dev/stderrKONG_ADMIN_ERROR_LOG:/dev/stderrKONG_ADMIN_LISTEN:0.0.0.0:8001ports:-"8000:8000"-"8001:8001"depends_on:banking-api-service:condition:service_startedrestart:trueopa:condition:service_startedvolumes:-./infra/kong/kong.yml:/etc/kong/kong.yml:ro-./infra/kong/plugins/opa-authz:/usr/local/share/lua/5.1/kong/plugins/opa-authz:ro关键环境变量:
KONG_DATABASE=off—— Kong 以无数据库(DB-less)模式运行。所有配置来自文件,而非数据库。KONG_DECLARATIVE_CONFIG=/etc/kong/kong.yml—— 保存 Kong 声明式路由配置的文件。KONG_PLUGINS=bundled,opa-authz—— 启用 Kong 内置插件以及自定义的opa-authz插件。KONG_ADMIN_LISTEN=0.0.0.0:8001—— 在容器内暴露 Kong admin API。
关键卷:
kong.yml以只读方式挂载到容器内的/etc/kong/kong.yml。- 自定义插件目录以只读方式挂载到
/usr/local/share/lua/5.1/kong/plugins/opa-authz—— 这是 Kong 扫描的标准 Lua 插件路径。
depends_on意味着 Kong 仅在banking-api-service与opa启动之后才启动。若banking-api-service重启,Kong 也会重启(restart: true)。
infra/kong/kong.yml中的 Kong
该文件使用 Kong 的声明式格式(_format_version: "3.0"),并定义了一个 service,内联一个 route 与一个 plugin:
_format_version:"3.0"services:-name:banking-apiurl:http://banking-api-service:8080routes:-name:banking-api-routepaths:-/api/accountsstrip_path:falseplugins:-name:opa-authzconfig:opa_url:http://opa:8181/v1/data/banking_authz/allowintrospection_url:http://keycloak:8080/realms/banking-poc/protocol/openid-connect/token/introspectintrospection_client_id:kong-introspectionintrospection_client_secret:kong-introspection-secrettimeout_ms:2000Service
name: banking-api—— Kong 对上游的内部名称。url: http://banking-api-service:8080—— Kong 把被允许的请求转发到这里。
Route
paths: [/api/accounts]—— 以/api/accounts开头的请求匹配此路由。strip_path: false—— 路径按原样转发。
因此一个GET /api/accounts/A-1001的请求(来自alice或ops-admin)会按GET /api/accounts/A-1001转发到上游。
插件挂载与配置值
opa-authz插件在每个匹配该路由的请求上运行。它的配置提供了插件在运行时使用的实际 URL 与凭据:
opa_url—— Kong 把授权 input POST 过去的 OPA 端点。introspection_url—— Kong 调用来检查令牌活跃性的Keycloak端点。introspection_client_id—— 在Keycloak中注册的kong-introspectionclient。introspection_client_secret—— 该 client 配对的密钥。timeout_ms: 2000—— Kong 在中止前等待Keycloak或OPA的时长。
opa-authz插件
自定义插件位于infra/kong/plugins/opa-authz/,由两个文件组成:
schema.lua—— 配置定义
schema.lua声明了合法插件配置的形态。Kong 在启动时会用此 schema 校验任何opa-authz插件块:
return{name="opa-authz",fields={{config={type="record",fields={{opa_url={type="string",required=true}},{introspection_url={type="string",required=true}},{introspection_client_id={type="string",required=true}},{introspection_client_secret={type="string",required=true}},{timeout_ms={type="number",default=2000}},},},},},}- 四个 URL/凭据字段都是必填字符串 —— 任一缺失,Kong 都会拒绝启动。
timeout_ms是可选的,默认2000。schema.lua定义什么是被允许的;kong.yml提供实际值;handler.lua在运行时使用它们。
handler.lua—— 执行逻辑
handler.lua在 Kong 的access阶段(优先级900)对每个匹配的请求运行。逻辑按以下顺序执行:
第 1 步 —— 读取 bearer 令牌
localauth_header=kong.request.get_header("authorization")ifnotauth_headerthenreturnkong.response.exit(401,{message="missing bearer token"})endlocaltoken=auth_header:match("[Bb]earer%s+(.+)")ifnottokenthenreturnkong.response.exit(401,{message="invalid bearer token"})end若不存在Authorization头、或其中不含 bearer 令牌,则返回401。
第 2 步 —— 用Keycloak内省令牌
localintrospection,introspection_err=introspect_token(conf,token)ifnotintrospectionthenreturnkong.response.exit(503,{message="introspection unavailable",detail=introspection_err})endifintrospection.active~=truethenreturnkong.response.exit(401,{message="inactive token"})endKong 用introspection_client_id与introspection_client_secret以 HTTP Basic 认证,把令牌 POST 到Keycloak的内省端点。若Keycloak不可达,Kong 返回503。若令牌不是 active,Kong 返回401。
关于 Kong 为何使用内省而非基于 JWKS 的本地校验,见下文「为什么 Kong 使用内省而非 JWKS」。关于内省机制上如何工作,见 11 — JWT 签名、校验与内省。
第 3 步 —— 解码 JWT 声明
localclaims=decode_claims(token)ifnotclaimsthenreturnkong.response.exit(401,{message="unreadable jwt payload"})endKong 对 JWT 载荷段做 base64 解码并 JSON 解析以读取声明。这是一次本地解码 —— 没有额外的网络调用。
第 4 步 —— 确定有效角色
localfunctioneffective_role(claims)localrealm_access=claimsandclaims.realm_accesslocalroles=realm_accessandrealm_access.rolesor{}for_,roleinipairs(roles)doifrole=="ops-admin"thenreturn"ops-admin"endendfor_,roleinipairs(roles)doifrole=="customer"thenreturn"customer"endendreturnnilendops-admin优先于customer。若两个角色都不存在,effective_role返回nil。
第 5 步 —— 构建 OPA input 并调用OPA
localaccount_id=kong.request.get_path():match("/api/accounts/([^/]+)")localrequest_body=cjson.encode({input={method=kong.request.get_method(),path=kong.request.get_path(),account_id=account_id,customer_id=claim_value(claims.customer_id),account_ids=claim_values(claims.account_ids),role=effective_role(claims),username=claims.preferred_username,},})对于像alice调用GET /api/accounts/A-1001这样的请求,Kong 从路径中提取account_id = "A-1001",并把它与其他字段打包成一个 JSON input 文档。
第 6 步 —— 执行OPA决策
ifdecision.result~=truethenreturnkong.response.exit(403,{message="forbidden"})endOPA返回{"result": true}或{"result": false}。Kong 检查decision.result—— 任何非true(包括false或字段缺失)都会导致403。若OPA不可达或返回非 200 状态,Kong 返回503。
Kong 的请求流程
与其他组件的互操作
Kong 与banking-api-service
Kong 只转发那些同时通过了Keycloak内省与OPA授权的请求。banking-api-service不直接对外暴露 —— 来自alice或ops-admin的所有流量都经由 Kong 到达。
banking-api-service也会用 JWKS 自行校验 JWT。这是纵深防御:银行服务不会因为 Kong 已经检查过就完全信任。
Kong 与identity-bootstrap-service
Kong 并不位于identity-bootstrap-service之前。bootstrap 是一个内部演示工具,位于同一 Compose 网络上,但 Kong 不向它路由任何流量。
Kong 与Keycloak
Kong 使用Keycloak做令牌内省,而不是把它当作自身管理访问的身份提供方。
为什么 Kong 使用内省而非 JWKS
Kong 是边缘 PEP。在它用令牌声明构建 OPA input 之前,它需要从Keycloak得到一个实时确认:该令牌此刻仍然处于 active。
一个令牌可能:
- 仍然能被正确解码为 JWT,
- 仍然带有有效的密码学签名,
- 仍然处在其
exp过期窗口内,
但Keycloak可能因为登出、会话过期或吊销,已经认为它 inactive。
基于 JWKS 的本地校验能确认签名、签发者、受众与过期 —— 但它无法反映Keycloak当前的会话状态。
内省让 Kong 在决定调用 OPA 之前,直接从Keycloak得到「真相之源」的答案。
因此设计是:
- Kong 内省—— 在 OPA 之前、在边缘做实时令牌活跃性检查。
banking-api-serviceJWKS 校验—— 在资源服务器内部做快速的本地密码学校验。
关于内省如何工作的完整机制(HTTP 交互、Keycloak 的响应、active的含义),见 11 — JWT 签名、校验与内省。
Kong 与OPA
Kong 仅在令牌被确认 active 之后才调用OPA。OPA接收到:
| 字段 | 来源 |
|---|---|
method | 请求的 HTTP 方法 |
path | 请求路径 |
account_id | 从/api/accounts/{account_id}提取 |
customer_id | JWT 声明 |
account_ids | JWT 声明(列表) |
role | 推导得到:ops-admin|customer|nil |
username | JWT 的preferred_username声明 |
OPA返回{"result": true}或{"result": false}。Kong 据此执行。
把策略放在OPA里,意味着授权规则可以在不修改 Kong 或银行服务的情况下变更。
实用检查命令
从 Compose 网络内部发起请求时,使用辅助容器:
dockercomposeexeccurlsh检查Keycloak的 OIDC discovery:
dockercomposeexeccurlcurlhttp://keycloak:8080/realms/banking-poc/.well-known/openid-configuration检查OPA的策略端点:
dockercomposeexeccurlcurlhttp://opa:8181/v1/data/banking_authz/allow检查banking-api-service健康状态:
dockercomposeexeccurlcurlhttp://banking-api-service:8080/actuator/health从宿主机检查 Kong:
# Admin API — shows loaded routes, services, pluginscurlhttp://localhost:8001# Proxy — test the protected route (expect 401 without a token)curl-ihttp://localhost:8000/api/accounts/A-1001← Prev: 06 — Keycloak / IdP · Next: 08 — OPA →
📚 返回专栏目录