微服务认证与授权:08 — OPA(PDP)
08 — OPAPDP GitHub: https://github.com/geekchow/micro-service-authOPA是策略决策点Policy Decision Point它从Kong接收授权 input并返回allow或deny。什么是 OPAOPA是 Open Policy Agent 的缩写。在本项目中OPA是PDP策略决策点—— 完整的 IdP / PEP / PDP 术语表见 01 — 概念。OPA只做一件事接收一个结构化的 input 文档用 Rego 策略规则对其进行评估返回一个决策allow或denyOPA不认证用户。那是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 在架构中的位置identity and token activityallow or denybusiness responseClientKongKeycloakOPAbanking-api-serviceOPA位于Kong与上游banking-api-service之间。Kong 在每个请求上同步调用OPA、等待决策然后要么转发请求、要么返回403。Input → Policy → Result 模型OPA是一个通用引擎。它本身并不知道「银行账户」是什么。它只知道它收到了什么 inputRego 里写了什么规则Input JSONRego Policyresult: true or false在本仓库中Kong构造并发送inputbanking_authz.rego定义规则OPA返回result: true或result: falseRego 基础OPA策略用Rego编写这是一门声明式语言。你描述的是「要授予访问必须满足什么条件」而不是写一步步的命令式逻辑。实践中本仓库的规则读起来是如果是读路由且角色是ops-admin则允许如果是读路由、角色是customer、且请求的账户在令牌的account_ids中则允许其余一切拒绝默认拒绝Deny By Default策略中最重要的一行是default allow : false除非某条allow规则匹配否则答案就是deny。这比枚举 deny 规则更安全 —— 一条缺失的 deny 规则永远不会意外地放开访问。本仓库中的实际策略文件infra/opa/policies/banking_authz.regopackage 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在以下地址查询OPAhttp://opa:8181/v1/data/banking_authz/allow所以banking_authz是包allow是被查询的决策。default allow : false除非下面某条allow规则匹配否则一切都被拒绝。第一条allow规则 —— ops-adminallow { read_only_account_request input.role ops-admin }含义请求必须是合法的读路由通过辅助规则判定调用方必须具备角色ops-admin不做账户归属检查 ——ops-admin可读取任意账户第二条allow规则 —— customerallow { 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_requestread_only_account_request { input.method GET regex.match(^/api/accounts/[^/](?:/transactions)?$, input.path) }含义只有GET请求能通过此关卡只允许两种路径形态/api/accounts/{accountId}/api/accounts/{accountId}/transactions这防止未来新增的非读路由被同样的allow规则意外放行。OPA 接收到什么 InputOPA不直接读取 HTTP 请求。Kong构造一个结构化的 input 文档并以 JSON POST 过去。策略中OPA实际消费的字段是字段类型被谁使用methodstringread_only_account_requestpathstringread_only_account_requestrolestring两条allow规则customer_idstringcustomer 的allow规则account_ids字符串数组customer 的allow规则account_idstringcustomer 的allow规则username被包含在 input 中但在本版本的策略规则里未被消费。完整的声明目录见 14 — 请求与响应细节。alice的示例 input 文档{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 发给 OPAKong通过opa-authz插件调用OPA的 REST API。URL 来自infra/kong/kong.ymlplugins:-name:opa-authzconfig:opa_url:http://opa:8181/v1/data/banking_authz/allow插件 handlerinfra/kong/plugins/opa-authz/handler.lua构造 input 体localrequest_bodycjson.encode({input{methodkong.request.get_method(),pathkong.request.get_path(),account_idaccount_id,customer_idclaim_value(claims.customer_id),account_idsclaim_values(claims.account_ids),roleeffective_role(claims),usernameclaims.preferred_username,},})OPA收到的是从已校验 JWT 中提取出来的、干净的授权 input而不是原始 HTTP 请求。OPA 返回什么若策略允许{result:true}若策略拒绝{result:false}Kong把它映射为行为result: true→ 把请求转发给上游banking-api-serviceresult: false→ 向客户端返回403 Forbidden本 PoC 中的 OPA 请求流程banking-api-serviceOPAKeycloakKongClientbanking-api-serviceOPAKeycloakKongClientalt[result false][result true]GET /api/accounts/A-1001 JWTIntrospect tokenactive truePOST policy input JSONEvaluate Rego rulesresult true or false403 Forbiddenforward requestbanking response200 responseOPA 在 Docker Compose 中如何运行opa:image:openpolicyagent/opa:0.68.0command:[run,--server,--addr0.0.0.0:8181,/policies]ports:-8181:8181volumes:-./infra/opa/policies:/policies:roOPA作为独立的 HTTP 服务器运行在端口8181策略以只读方式从infra/opa/policies挂载OPA没有被编译进任何 Java 服务 —— 它是一个独立容器这种分离意味着策略可以独立于Kong或banking-api-service进行更新、测试与重载。测试文件infra/opa/policies/banking_authz_test.regopackage banking_authz_test import data.banking_authz允许ops-admin 读取某账户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 读取自己的账户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 读取自己的交易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_accountaccount_ids不包含请求的账户test_customer_without_claimed_account_is_deniedaccount_ids为空test_customer_without_customer_id_is_deniedcustomer_id字段缺失test_ops_admin_post_account_is_deniedPOST不满足read_only_account_requesttest_customer_subresource_path_is_denied/cards路径不被正则匹配test_other_roles_are_denied角色auditor不匹配任一allow规则负向测试与正向测试同等重要只有当你也证明了它拒绝什么时一条策略才值得信任。实际示例alice 读取她自己的账户Input{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 customercustomer_id非空A-1001在account_ids中alice 尝试访问另一个客户的账户Input{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{input:{method:GET,path:/api/accounts/A-2001,account_id:A-2001,customer_id:C-9999,role:ops-admin}}结果allow原因ops-admin规则不检查账户归属POST 请求对两种角色都拒绝Input{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{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-serviceOPA依赖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 → 返回专栏目录