4. 策略组合算法

好,咱们今天聊聊策略组合算法。很多刚接触 ABAC 的同学都会问一句话:「如果一个策略允许了,另一个策略拒绝了,到底听谁的?」 这个问题,恰恰就是策略组合算法要解决的。

我在项目中见过不少团队,策略写得洋洋洒洒几十条,结果一上线发现有的请求被误拒,有的不该放的却放了。一查原因,全都是因为没搞懂策略组合的优先级。说白了,策略组合算法就是 ABAC 引擎里的交通规则——谁先走、谁让行,必须清清楚楚。

为什么需要策略组合算法?

一个 ABAC 策略集通常包含多条策略,每条策略都有自己的 Target、Condition 和 Effect(Permit 或 Deny)。当一条请求命中多个策略时,矛盾就出现了。策略组合算法定义了这些矛盾的裁决逻辑。

常见的组合算法有五种:

  • Deny-overrides(拒绝优先):只要有一条策略裁决为 Deny,最终结果就是 Deny。
  • Permit-overrides(允许优先):只要有一条策略裁决为 Permit,最终结果就是 Permit。
  • First-applicable(第一个适用):按顺序找到第一条命中的策略,取它的 Effect。
  • Only-one-applicable(唯一适用):如果命中的策略多于一条,直接返回 Indeterminate(不确定)。
  • Ordered-deny-overrides / Ordered-permit-overrides:带顺序的拒绝优先/允许优先。

Deny-overrides:我最常用的算法

我个人习惯在高安全场景下使用 Deny-overrides。比如金融系统的转账接口,任何一条策略只要认为「该操作有风险」,哪怕其他十条策略都说可以,我也必须拒绝。为什么?因为安全是木桶效应,最短的那块板决定了结果。

核心逻辑: 遍历所有命中策略,遇到第一个 Deny 直接返回 Deny;全部遍历完没有 Deny,再返回 Permit。如果既有 Deny 又有 Permit,Deny 胜出。

我曾在一个银行项目中遇到过这样的坑:我们把用户隐私策略和业务策略写在了同一个策略集里,用了默认的 Permit-overrides。结果某次运维误操作,把一条「禁止外网访问数据库」的策略加到了业务策略里,但因为 Permit-overrides,所有外网请求居然通过了……还好是在测试环境发现的。从那以后,凡是涉及安全、合规的策略集,我全部强制换成 Deny-overrides。

Permit-overrides:适合开放场景

Permit-overrides 正好反过来。只要有一条策略说「可以」,就允许。 这种算法适合什么场景?比如企业内部的门禁系统,只要你有 VIP 卡、临时访客码、或者领导的授权短信,任何一种方式验证通过,就可以进门。你想想看,如果这时候用 Deny-overrides,那门禁就太死板了。

小提示: Permit-overrides 容易出现「一个漏洞绕过所有控制」的风险。建议只在策略数量可控、策略编写审核严格的环境下使用。

我曾经在做一个 SaaS 平台时,租户管理员可以自定义权限策略。默认我们用了 Permit-overrides,结果有个租户不小心写了一条「允许所有操作」的宽泛策略,和别的策略混在一起,瞬间把整个租户的权限放开了。嗯,后来我们加了策略有效性检查,并且在租户级别强制使用 Deny-overrides 兜底。

First-applicable:顺序即优先级

First-applicable 算法最常见于 XACML 的策略组合中。策略列表是有序的,引擎从上到下依次匹配,匹配到第一条就返回其结果。 这样设计有一个好处:你可以把最具体的策略放在前面,通用的兜底策略放在最后。

举个例子:

策略1: 如果角色是 "admin" → Permit
策略2: 如果角色是 "user" 且操作是 "read" → Permit
策略3: 否则不论什么情况 → Deny

用 First-applicable,管理员直接放行;普通用户只能读;其他任何操作都被 Deny 兜底。逻辑很清晰,对吧?

Only-one-applicable:强制唯一命中

这个算法比较特殊,它要求对每条请求最多只能有一条策略命中。如果命中多条,引擎直接返回 Indeterminate(不确定)。你会问,这不是有点极端吗?其实它在一些需要强制互斥的场景下很有用,比如「你不能既是内部员工又是外部顾问」的权限设计。

注意: 使用 Only-one-applicable 时,一定要做好策略梳理,避免策略间有重叠。否则你的用户会经常收到「权限判定失败」的报错。

Ordered-deny-overrides 与 Ordered-permit-overrides

这两个是带顺序的变体。普通 Deny-overrides 和 Permit-overrides 在遇到多个冲突结果时,通常会返回 Indeterminate(因为无法确定谁先谁后)。而带顺序的版本会按照策略列表的顺序来决定优先级。例如 Ordered-deny-overrides 遇到多个 Deny 时,取列表中第一个 Deny 的结果;如果没有 Deny,则按列表顺序取第一个 Permit。

我个人觉得这个算法有点「四不像」,既不像纯 Deny-overrides 那么,也不像 First-applicable 那么干脆。但我在 IAM 厂商的 SDK 里见过它的身影,用来处理不同管理员添加的策略之间的冲突。

算法对比表格

算法名称优先结果多策略冲突适用场景
Deny-overridesDenyDeny 胜出安全审计、合规检查
Permit-overridesPermitPermit 胜出开放合作、应急放行
First-applicable按顺序取首个命中细粒度分层控制
Only-one-applicable唯一命中返回 Indeterminate互斥角色、严格分组
Ordered-deny-overrides顺序+Deny优先按顺序取首次多管理源的策略合并

SVG:策略组合算法决策流程

下面这张图展示了从请求命中多条策略到最终裁决的全过程,不同算法走不同分支。

策略组合算法决策流程 请求命中 N 条策略 根据策略集配置 选择组合算法 Deny-overrides 有 Deny → Deny 全 Permit → Permit Permit-overrides 有 Permit → Permit 全 Deny → Deny First-applicable 按顺序取首个 命中策略的结果 Only-one-applicable N=1 → 取结果 N>1 → Indeterminate Ordered-deny -overrides 按顺序取首次 Deny; 无则取Permit Ordered-permit -overrides 按顺序取首次 Permit; 无则取Deny 最终裁决:Permit / Deny / Indeterminate / NotApplicable 注意 不同算法下相同的策略集可能产生不同结果 选择算法时需要结合业务场景和安全需求

避坑指南:我曾经踩过的坑

  • 策略合并时忘记指定算法: 大多数 ABAC 引擎默认使用 Deny-overrides 或 Permit-overrides(取决于产品)。我遇到过一个团队直接用了 XACML 的默认值,结果发现有的场景下 Deny 没有优先,白白漏掉了安全规则。建议每个策略集都显式声明组合算法。
  • Indeterminate 处理不当: 当算法无法裁决时(如 Only-one-applicable 遇到多条命中),引擎返回 Indeterminate。很多开发者直接忽略这个结果,相当于放行了请求。我在审计时发现过一次,赶紧补上了「Indeterminate 视为 Deny」的兜底逻辑。
  • Ordered 算法与策略顺序的耦合: 如果你用了 Ordered 变体,那么策略的排序就决定了最终结果。有时候运维人员在 UI 上拖拽调整策略顺序,自己却没察觉,导致权限漂移。我的建议是:给策略加一个显式的 priority 字段,不要依赖数组下标。

如何选择?我的个人经验判断

我给几个简单的判断标准:

  • 如果整个系统的最高原则是安全(比如支付、医疗数据),无脑选 Deny-overrides。
  • 如果用户体验优先,而且你有严格的策略审核机制,可以考虑 Permit-overrides。
  • 如果策略是分层细粒度控制(比如不同角色、不同资源),First-applicable 是最清晰的。
  • 如果策略集来源不同管理域(如租户 + 平台),且你想避免冲突,Only-one-applicable 是一个强制隔离手段。
一点心里话: 没有「最好」的算法,只有最适合当前场景的算法。我一般会在系统设计阶段,拉上安全和业务方一起画一张决策树,像上面那张 SVG 一样,把每个分支的结果跑一遍。这一步做扎实了,后边写代码时才不会迷茫。