08 --- OPA(PDP)
📦 GitHub: https://github.com/geekchow/micro-service-auth
OPA 是策略决策点(Policy Decision Point):它从 Kong 接收授权 input,并返回 allow 或 deny。
什么是 OPA
OPA 是 Open Policy Agent 的缩写。
在本项目中,OPA 是 PDP(策略决策点)------ 完整的 IdP / PEP / PDP 术语表见 01 --- 概念。
OPA 只做一件事:
- 接收一个结构化的 input 文档
- 用 Rego 策略规则对其进行评估
- 返回一个决策:
allow或deny
OPA 不认证用户。那是 Keycloak 的职责。
OPA 不在边缘执行决策。那是 Kong 的职责。
OPA 只负责判定。
本项目为什么需要 OPA
授权逻辑本可以直接写在:
Kong的插件代码里banking-api-service的 Java 代码里
但那样会把策略与执行或业务逻辑耦合在一起。
把 OPA 用作专职的 PDP 带来三点具体好处:
- 策略与网关、服务代码相互分离。
- 策略可以用
rego test独立测试。 - 策略可以在不重写
Kong插件或banking-api-service的情况下变更。
在本 PoC 中:
Keycloak证明用户是谁。Kong在边缘执行(PEP)。OPA判定操作是否被允许(PDP)。banking-api-service提供纵深防御(资源服务器)。
OPA 在架构中的位置
#mermaid-svg-t5NZX25YiVoz9Pf5{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-t5NZX25YiVoz9Pf5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-t5NZX25YiVoz9Pf5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-t5NZX25YiVoz9Pf5 .error-icon{fill:#552222;}#mermaid-svg-t5NZX25YiVoz9Pf5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-t5NZX25YiVoz9Pf5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-t5NZX25YiVoz9Pf5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-t5NZX25YiVoz9Pf5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-t5NZX25YiVoz9Pf5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-t5NZX25YiVoz9Pf5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-t5NZX25YiVoz9Pf5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-t5NZX25YiVoz9Pf5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-t5NZX25YiVoz9Pf5 .marker.cross{stroke:#333333;}#mermaid-svg-t5NZX25YiVoz9Pf5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-t5NZX25YiVoz9Pf5 p{margin:0;}#mermaid-svg-t5NZX25YiVoz9Pf5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-t5NZX25YiVoz9Pf5 .cluster-label text{fill:#333;}#mermaid-svg-t5NZX25YiVoz9Pf5 .cluster-label span{color:#333;}#mermaid-svg-t5NZX25YiVoz9Pf5 .cluster-label span p{background-color:transparent;}#mermaid-svg-t5NZX25YiVoz9Pf5 .label text,#mermaid-svg-t5NZX25YiVoz9Pf5 span{fill:#333;color:#333;}#mermaid-svg-t5NZX25YiVoz9Pf5 .node rect,#mermaid-svg-t5NZX25YiVoz9Pf5 .node circle,#mermaid-svg-t5NZX25YiVoz9Pf5 .node ellipse,#mermaid-svg-t5NZX25YiVoz9Pf5 .node polygon,#mermaid-svg-t5NZX25YiVoz9Pf5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-t5NZX25YiVoz9Pf5 .rough-node .label text,#mermaid-svg-t5NZX25YiVoz9Pf5 .node .label text,#mermaid-svg-t5NZX25YiVoz9Pf5 .image-shape .label,#mermaid-svg-t5NZX25YiVoz9Pf5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-t5NZX25YiVoz9Pf5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-t5NZX25YiVoz9Pf5 .rough-node .label,#mermaid-svg-t5NZX25YiVoz9Pf5 .node .label,#mermaid-svg-t5NZX25YiVoz9Pf5 .image-shape .label,#mermaid-svg-t5NZX25YiVoz9Pf5 .icon-shape .label{text-align:center;}#mermaid-svg-t5NZX25YiVoz9Pf5 .node.clickable{cursor:pointer;}#mermaid-svg-t5NZX25YiVoz9Pf5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-t5NZX25YiVoz9Pf5 .arrowheadPath{fill:#333333;}#mermaid-svg-t5NZX25YiVoz9Pf5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-t5NZX25YiVoz9Pf5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-t5NZX25YiVoz9Pf5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-t5NZX25YiVoz9Pf5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-t5NZX25YiVoz9Pf5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-t5NZX25YiVoz9Pf5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-t5NZX25YiVoz9Pf5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-t5NZX25YiVoz9Pf5 .cluster text{fill:#333;}#mermaid-svg-t5NZX25YiVoz9Pf5 .cluster span{color:#333;}#mermaid-svg-t5NZX25YiVoz9Pf5 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-t5NZX25YiVoz9Pf5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-t5NZX25YiVoz9Pf5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-t5NZX25YiVoz9Pf5 .icon-shape,#mermaid-svg-t5NZX25YiVoz9Pf5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-t5NZX25YiVoz9Pf5 .icon-shape p,#mermaid-svg-t5NZX25YiVoz9Pf5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-t5NZX25YiVoz9Pf5 .icon-shape .label rect,#mermaid-svg-t5NZX25YiVoz9Pf5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-t5NZX25YiVoz9Pf5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-t5NZX25YiVoz9Pf5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-t5NZX25YiVoz9Pf5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} identity and token activity
allow or deny
business response
Client
Kong
Keycloak
OPA
banking-api-service
OPA 位于 Kong 与上游 banking-api-service 之间。Kong 在每个请求上同步调用 OPA、等待决策,然后要么转发请求、要么返回 403。
Input → Policy → Result 模型
OPA 是一个通用引擎。它本身并不知道「银行账户」是什么。它只知道:
- 它收到了什么 input
- Rego 里写了什么规则
#mermaid-svg-nl9Op1UNjfrgbIQm{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-nl9Op1UNjfrgbIQm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nl9Op1UNjfrgbIQm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nl9Op1UNjfrgbIQm .error-icon{fill:#552222;}#mermaid-svg-nl9Op1UNjfrgbIQm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nl9Op1UNjfrgbIQm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nl9Op1UNjfrgbIQm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nl9Op1UNjfrgbIQm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nl9Op1UNjfrgbIQm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nl9Op1UNjfrgbIQm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nl9Op1UNjfrgbIQm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nl9Op1UNjfrgbIQm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nl9Op1UNjfrgbIQm .marker.cross{stroke:#333333;}#mermaid-svg-nl9Op1UNjfrgbIQm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nl9Op1UNjfrgbIQm p{margin:0;}#mermaid-svg-nl9Op1UNjfrgbIQm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-nl9Op1UNjfrgbIQm .cluster-label text{fill:#333;}#mermaid-svg-nl9Op1UNjfrgbIQm .cluster-label span{color:#333;}#mermaid-svg-nl9Op1UNjfrgbIQm .cluster-label span p{background-color:transparent;}#mermaid-svg-nl9Op1UNjfrgbIQm .label text,#mermaid-svg-nl9Op1UNjfrgbIQm span{fill:#333;color:#333;}#mermaid-svg-nl9Op1UNjfrgbIQm .node rect,#mermaid-svg-nl9Op1UNjfrgbIQm .node circle,#mermaid-svg-nl9Op1UNjfrgbIQm .node ellipse,#mermaid-svg-nl9Op1UNjfrgbIQm .node polygon,#mermaid-svg-nl9Op1UNjfrgbIQm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-nl9Op1UNjfrgbIQm .rough-node .label text,#mermaid-svg-nl9Op1UNjfrgbIQm .node .label text,#mermaid-svg-nl9Op1UNjfrgbIQm .image-shape .label,#mermaid-svg-nl9Op1UNjfrgbIQm .icon-shape .label{text-anchor:middle;}#mermaid-svg-nl9Op1UNjfrgbIQm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-nl9Op1UNjfrgbIQm .rough-node .label,#mermaid-svg-nl9Op1UNjfrgbIQm .node .label,#mermaid-svg-nl9Op1UNjfrgbIQm .image-shape .label,#mermaid-svg-nl9Op1UNjfrgbIQm .icon-shape .label{text-align:center;}#mermaid-svg-nl9Op1UNjfrgbIQm .node.clickable{cursor:pointer;}#mermaid-svg-nl9Op1UNjfrgbIQm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-nl9Op1UNjfrgbIQm .arrowheadPath{fill:#333333;}#mermaid-svg-nl9Op1UNjfrgbIQm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-nl9Op1UNjfrgbIQm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-nl9Op1UNjfrgbIQm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nl9Op1UNjfrgbIQm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-nl9Op1UNjfrgbIQm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nl9Op1UNjfrgbIQm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-nl9Op1UNjfrgbIQm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-nl9Op1UNjfrgbIQm .cluster text{fill:#333;}#mermaid-svg-nl9Op1UNjfrgbIQm .cluster span{color:#333;}#mermaid-svg-nl9Op1UNjfrgbIQm 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-nl9Op1UNjfrgbIQm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-nl9Op1UNjfrgbIQm rect.text{fill:none;stroke-width:0;}#mermaid-svg-nl9Op1UNjfrgbIQm .icon-shape,#mermaid-svg-nl9Op1UNjfrgbIQm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nl9Op1UNjfrgbIQm .icon-shape p,#mermaid-svg-nl9Op1UNjfrgbIQm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-nl9Op1UNjfrgbIQm .icon-shape .label rect,#mermaid-svg-nl9Op1UNjfrgbIQm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nl9Op1UNjfrgbIQm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-nl9Op1UNjfrgbIQm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-nl9Op1UNjfrgbIQm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Input JSON
Rego Policy
result: true or false
在本仓库中:
Kong构造并发送inputbanking_authz.rego定义规则OPA返回result: true或result: false
Rego 基础
OPA 策略用 Rego 编写,这是一门声明式语言。
你描述的是「要授予访问,必须满足什么条件」,而不是写一步步的命令式逻辑。
实践中,本仓库的规则读起来是:
- 如果是读路由且角色是
ops-admin,则允许 - 如果是读路由、角色是
customer、且请求的账户在令牌的account_ids中,则允许 - 其余一切拒绝
默认拒绝(Deny By Default)
策略中最重要的一行是:
rego
default allow := false
除非某条 allow 规则匹配,否则答案就是 deny。这比枚举 deny 规则更安全 ------ 一条缺失的 deny 规则永远不会意外地放开访问。
本仓库中的实际策略
文件:infra/opa/policies/banking_authz.rego
rego
package banking_authz
default allow := false
allow {
read_only_account_request
input.role == "ops-admin"
}
allow {
read_only_account_request
input.role == "customer"
input.customer_id != ""
account_ids := object.get(input, "account_ids", [])
account_ids[_] == input.account_id
}
read_only_account_request {
input.method == "GET"
regex.match("^/api/accounts/[^/]+(?:/transactions)?$", input.path)
}
策略逐段讲解
package banking_authz
把规则放进 banking_authz 包。Kong 在以下地址查询 OPA:
http://opa:8181/v1/data/banking_authz/allow
所以 banking_authz 是包,allow 是被查询的决策。
default allow := false
除非下面某条 allow 规则匹配,否则一切都被拒绝。
第一条 allow 规则 ------ ops-admin
rego
allow {
read_only_account_request
input.role == "ops-admin"
}
含义:
- 请求必须是合法的读路由(通过辅助规则判定)
- 调用方必须具备角色
ops-admin - 不做账户归属检查 ------
ops-admin可读取任意账户
第二条 allow 规则 ------ customer
rego
allow {
read_only_account_request
input.role == "customer"
input.customer_id != ""
account_ids := object.get(input, "account_ids", [])
account_ids[_] == input.account_id
}
含义:
- 请求必须是合法的读路由
- 调用方必须具备角色
customer - 令牌必须携带非空的
customer_id - 请求的
account_id必须出现在令牌的account_ids列表中
这就是核心的客户归属检查。
辅助规则:read_only_account_request
rego
read_only_account_request {
input.method == "GET"
regex.match("^/api/accounts/[^/]+(?:/transactions)?$", input.path)
}
含义:
- 只有
GET请求能通过此关卡 - 只允许两种路径形态:
/api/accounts/{accountId}/api/accounts/{accountId}/transactions
这防止未来新增的非读路由被同样的 allow 规则意外放行。
OPA 接收到什么 Input
OPA 不直接读取 HTTP 请求。Kong 构造一个结构化的 input 文档并以 JSON POST 过去。
策略中 OPA 实际消费的字段是:
| 字段 | 类型 | 被谁使用 |
|---|---|---|
method |
string | read_only_account_request |
path |
string | read_only_account_request |
role |
string | 两条 allow 规则 |
customer_id |
string | customer 的 allow 规则 |
account_ids |
字符串数组 | customer 的 allow 规则 |
account_id |
string | customer 的 allow 规则 |
username 被包含在 input 中,但在本版本的策略规则里未被消费。
完整的声明目录见 14 --- 请求与响应细节。
alice 的示例 input 文档:
json
{
"input": {
"method": "GET",
"path": "/api/accounts/A-1001",
"account_id": "A-1001",
"customer_id": "C-1001",
"account_ids": ["A-1001"],
"role": "customer",
"username": "alice"
}
}
Kong 如何把 Input 发给 OPA
Kong 通过 opa-authz 插件调用 OPA 的 REST API。URL 来自 infra/kong/kong.yml:
yaml
plugins:
- name: opa-authz
config:
opa_url: http://opa:8181/v1/data/banking_authz/allow
插件 handler(infra/kong/plugins/opa-authz/handler.lua)构造 input 体:
lua
local request_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,
},
})
OPA 收到的是从已校验 JWT 中提取出来的、干净的授权 input,而不是原始 HTTP 请求。
OPA 返回什么
若策略允许:
json
{ "result": true }
若策略拒绝:
json
{ "result": false }
Kong 把它映射为行为:
result: true→ 把请求转发给上游banking-api-serviceresult: false→ 向客户端返回403 Forbidden
本 PoC 中的 OPA 请求流程
banking-api-service OPA Keycloak Kong Client banking-api-service OPA Keycloak Kong Client #mermaid-svg-o6JliGwhyuz0ENgF{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-o6JliGwhyuz0ENgF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-o6JliGwhyuz0ENgF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-o6JliGwhyuz0ENgF .error-icon{fill:#552222;}#mermaid-svg-o6JliGwhyuz0ENgF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-o6JliGwhyuz0ENgF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-o6JliGwhyuz0ENgF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-o6JliGwhyuz0ENgF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-o6JliGwhyuz0ENgF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-o6JliGwhyuz0ENgF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-o6JliGwhyuz0ENgF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-o6JliGwhyuz0ENgF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-o6JliGwhyuz0ENgF .marker.cross{stroke:#333333;}#mermaid-svg-o6JliGwhyuz0ENgF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-o6JliGwhyuz0ENgF p{margin:0;}#mermaid-svg-o6JliGwhyuz0ENgF .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-o6JliGwhyuz0ENgF text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-o6JliGwhyuz0ENgF .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-o6JliGwhyuz0ENgF .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-o6JliGwhyuz0ENgF .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-o6JliGwhyuz0ENgF .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-o6JliGwhyuz0ENgF #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-o6JliGwhyuz0ENgF .sequenceNumber{fill:white;}#mermaid-svg-o6JliGwhyuz0ENgF #sequencenumber{fill:#333;}#mermaid-svg-o6JliGwhyuz0ENgF #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-o6JliGwhyuz0ENgF .messageText{fill:#333;stroke:none;}#mermaid-svg-o6JliGwhyuz0ENgF .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-o6JliGwhyuz0ENgF .labelText,#mermaid-svg-o6JliGwhyuz0ENgF .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-o6JliGwhyuz0ENgF .loopText,#mermaid-svg-o6JliGwhyuz0ENgF .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-o6JliGwhyuz0ENgF .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-o6JliGwhyuz0ENgF .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-o6JliGwhyuz0ENgF .noteText,#mermaid-svg-o6JliGwhyuz0ENgF .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-o6JliGwhyuz0ENgF .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-o6JliGwhyuz0ENgF .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-o6JliGwhyuz0ENgF .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-o6JliGwhyuz0ENgF .actorPopupMenu{position:absolute;}#mermaid-svg-o6JliGwhyuz0ENgF .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-o6JliGwhyuz0ENgF .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-o6JliGwhyuz0ENgF .actor-man circle,#mermaid-svg-o6JliGwhyuz0ENgF line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-o6JliGwhyuz0ENgF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt result false result true GET /api/accounts/A-1001 + JWT Introspect token active true POST policy input JSON Evaluate Rego rules result true or false 403 Forbidden forward request banking response 200 response
OPA 在 Docker Compose 中如何运行
yaml
opa:
image: openpolicyagent/opa:0.68.0
command: ["run", "--server", "--addr=0.0.0.0:8181", "/policies"]
ports:
- "8181:8181"
volumes:
- ./infra/opa/policies:/policies:ro
OPA作为独立的 HTTP 服务器运行在端口8181- 策略以只读方式从
infra/opa/policies挂载 OPA没有被编译进任何 Java 服务 ------ 它是一个独立容器
这种分离意味着策略可以独立于 Kong 或 banking-api-service 进行更新、测试与重载。
测试
文件:infra/opa/policies/banking_authz_test.rego
rego
package banking_authz_test
import data.banking_authz
允许:ops-admin 读取某账户
rego
test_ops_admin_is_allowed {
banking_authz.allow with input as {
"method": "GET",
"path": "/api/accounts/A-1001",
"role": "ops-admin",
"account_id": "A-1001",
"customer_id": "C-9999",
}
}
ops-admin 可读取任意账户端点,无论客户归属如何。
允许:customer 读取自己的账户
rego
test_customer_can_access_owned_account {
banking_authz.allow with input as {
"method": "GET",
"path": "/api/accounts/A-1001",
"role": "customer",
"account_id": "A-1001",
"customer_id": "C-1001",
"account_ids": ["A-1001"],
}
}
当账户 A-1001 出现在 alice(客户 C-1001)的 account_ids 中时,她可读取该账户。
允许:customer 读取自己的交易
rego
test_customer_can_access_owned_account_transactions {
banking_authz.allow with input as {
"method": "GET",
"path": "/api/accounts/A-1001/transactions",
"role": "customer",
"account_id": "A-1001",
"customer_id": "C-1001",
"account_ids": ["A-1001"],
}
}
对所拥有的账户,其交易子资源同样被允许。
拒绝用例
测试文件证明了以下所有情形会被拒绝:
| 测试 | 它证明了什么 |
|---|---|
test_customer_cannot_access_other_account |
account_ids 不包含请求的账户 |
test_customer_without_claimed_account_is_denied |
account_ids 为空 |
test_customer_without_customer_id_is_denied |
customer_id 字段缺失 |
test_ops_admin_post_account_is_denied |
POST 不满足 read_only_account_request |
test_customer_subresource_path_is_denied |
/cards 路径不被正则匹配 |
test_other_roles_are_denied |
角色 auditor 不匹配任一 allow 规则 |
负向测试与正向测试同等重要:只有当你也证明了它拒绝什么时,一条策略才值得信任。
实际示例
alice 读取她自己的账户
Input:
json
{
"input": {
"method": "GET",
"path": "/api/accounts/A-1001",
"account_id": "A-1001",
"customer_id": "C-1001",
"account_ids": ["A-1001"],
"role": "customer",
"username": "alice"
}
}
结果:allow
原因:
GET+ 匹配的路径 →read_only_account_request通过role == "customer"customer_id非空A-1001在account_ids中
alice 尝试访问另一个客户的账户
Input:
json
{
"input": {
"method": "GET",
"path": "/api/accounts/A-2001",
"account_id": "A-2001",
"customer_id": "C-1001",
"account_ids": ["A-1001"],
"role": "customer",
"username": "alice"
}
}
结果:deny
原因:
A-2001不在alice的account_ids(["A-1001"])中
ops-admin 读取任意账户
Input:
json
{
"input": {
"method": "GET",
"path": "/api/accounts/A-2001",
"account_id": "A-2001",
"customer_id": "C-9999",
"role": "ops-admin"
}
}
结果:allow
原因:
ops-admin规则不检查账户归属
POST 请求(对两种角色都拒绝)
Input:
json
{
"input": {
"method": "POST",
"path": "/api/accounts/A-1001",
"role": "ops-admin",
"account_id": "A-1001",
"customer_id": "C-9999"
}
}
结果:deny
原因:
read_only_account_request失败,因为method != "GET"
不受支持的子资源路径
Input:
json
{
"input": {
"method": "GET",
"path": "/api/accounts/A-1001/cards",
"role": "customer",
"account_id": "A-1001",
"customer_id": "C-1001",
"account_ids": ["A-1001"]
}
}
结果:deny
原因:
- 正则只匹配
/api/accounts/{id}与/api/accounts/{id}/transactions /cards不匹配
OPA 不做什么
OPA 很强大,但在本 PoC 中它有清晰的边界:
- 不认证用户(那是
Keycloak) - 不签发 JWT(那是
Keycloak) - 在本请求路径中不自己内省令牌(那是
Kong插件) - 在这里不自己校验 JWT 签名(
Kong插件在内省后解码载荷) - 不提供银行数据(那是
banking-api-service)
OPA 依赖 Kong 提供可信、格式良好的 input。如果 input 是错的,决策就是错的。
思维模型
Keycloak说明用户是谁。Kong验证令牌处于 active 并提取声明。Kong把结构化 input 发给OPA。OPA评估 Rego 规则并返回result: true或result: false。Kong在边缘执行该决策。banking-api-service在返回银行数据前再次校验 JWT。
← Prev: 07 --- Kong · Next: 09 --- banking-api-service →
📚 返回专栏目录