在企业级系统里,权限管理是绕不开的话题。早期大家用ACL做简单的资源授权,后来普遍转向RBAC,把用户归入角色、角色挂上权限,简单直接。但业务一旦复杂起来,问题就会暴露:同一个客服角色,工作时间内可以导出订单,下班后不行;同一个运营角色,只能查看自己负责的渠道数据;安全团队还要求所有敏感操作必须留痕可审计。这些带时间窗口、带数据范围、带上下文条件的权限要求,靠在角色表上不停加开关和标志位来维护,代码很快就会变成一团乱麻。PBAC(Policy-Based Access Control,基于策略的访问控制)正是针对这类场景的解法:把权限判断从散落各处的分支逻辑里抽出来,统一建模成一条条可管理的策略,交给独立的策略引擎求值,让授权逻辑变得集中、可组合、可审计。

PBAC的核心思想:把权限判断收敛为策略求值
PBAC的出发点很朴素:一次权限判断本质上是一次决策,决策需要输入(谁、要对什么资源、做什么操作、处在什么环境),也需要输出(允许或拒绝)。既然如此,不如把决策依据显式地写成一条条策略,每条策略描述在什么条件下、对什么主体、对什么资源上的什么动作,应该产生什么效果。运行时由策略引擎统一加载策略集,对每次请求做匹配和求值,业务代码只管拿结论。
这个模式里有两个关键角色需要认识。一个是PEP(Policy Enforcement Point,策略执行点),部署在业务链路里,负责拦截请求、组装上下文、调用决策服务并强制执行结果;另一个是PDP(Policy Decision Point,策略决策点),也就是策略引擎本身,只负责根据策略集给出结论,不掺和业务细节。两者分离之后,业务代码里不再出现具体的权限判断,只剩一个统一的拦截调用,权限规则调整也不再需要改代码发版。
这种收敛带来的好处是结构性的。策略可以独立做版本管理,像代码一样评审、测试、灰度上线;每次决策天然形成审计日志,合规检查有据可查;当规则需要跨系统复用时,只需要让不同系统的PEP对接同一个PDP,而不是各自复制一套判断逻辑再各自修bug。
PBAC、RBAC、ABAC放在一起看才清楚
不少资料把PBAC和RBAC、ABAC并列成三种互斥模型,这其实是个常见误区。更准确的理解是:RBAC和ABAC回答的是用什么要素做决策(角色或属性),PBAC回答的是用什么形式组织和管理这些决策依据(策略)。一条PBAC策略的匹配条件里完全可以引用角色,也可以引用属性,所以PBAC更像一个框架和容器,能把前两者的能力都装进来。三者的直观对比如下:
| 对比维度 | RBAC | ABAC | PBAC |
|---|---|---|---|
| 授权载体 | 角色 | 属性与关系 | 策略 |
| 表达能力 | 静态、偏粗粒度 | 动态、细粒度 | 由策略内容决定,可粗可细 |
| 规则可读性 | 直观 | 规则分散、排查困难 | 策略集中、可审计 |
| 典型实现 | 数据库角色权限表 | XACML | OPA、Casbin、AWS IAM |
实际选型时差异体现得很明显。RBAC胜在简单直观,权限关系一目了然,适合权限维度单一的中后台系统;ABAC表达能力强,能写出很动态的规则,但规则散落在代码各处,排查一个用户为什么没有权限时往往要翻半天日志;PBAC在表达能力和可管理性之间取了平衡,策略集中存储、集中求值,排查时只需要看这次请求命中的是哪几条策略。AWS的IAM、开源的OPA(Open Policy Agent)、Casbin,本质上都是PBAC思路的落地。
拆解一条完整的PBAC策略
一条典型的策略由五个部分组成:主体、动作、资源、条件和效果。主体描述谁在发起请求,可以是用户ID、角色标识、部门甚至客户端类型;动作描述要执行的操作,比如查看、导出、删除;资源描述操作对象,通常用类型加标识的形式,比如order:1000231;条件是可选的附加约束,比如时间窗口、IP段、数据归属;效果只有两种取值,Allow或Deny。下面是一条用JSON描述的策略,语义是售后客服仅在工作时间可查看和导出订单。
{
"policyId": "worktime-order-export",
"effect": "Allow",
"subjects": ["role:customer_service"],
"actions": ["order:view", "order:export"],
"resources": ["order:*"],
"condition": {
"type": "and",
"criterias": [
{"attr": "env.currentTime", "op": "between", "value": ["09:00", "18:00"]},
{"attr": "subject.department", "op": "eq", "value": "after_sales"}
]
},
"priority": 10,
"description": "售后客服仅在工作时间可查看和导出订单"
}条件部分值得多说一句。真实系统里的条件往往不是简单的一元判断,而是多个判断的组合,所以条件通常被建模成一棵表达式树:and节点要求所有子条件满足,or节点要求任一满足,叶子节点则是具体判断,比如某个属性等于某个值、落在某个区间。这种结构让再复杂的规则也能被统一解析和求值,也为前端做可视化规则编辑器留出了空间。
多条策略同时命中时如何裁决,是PBAC设计里必须明确的点。业界通行的原则是Deny优先:只要有一条Deny策略命中,最终结果就是拒绝;只有没有任何Deny命中且至少一条Allow命中时才放行;一条策略都没命中同样视为拒绝,也就是默认拒绝。这套裁决规则简单可预测,也是AWS IAM等主流实现采用的方式。
动手实现一个最小可用的策略引擎
理解原理之后,用一个百行以内的Python实现把流程串起来并不难。下面的代码包含三个类:Request表示一次访问请求,Policy表示一条策略,PolicyEngine负责加载策略并按Deny优先原则裁决。
class Request:
# 一次访问请求:谁、对什么资源、做什么操作、附带什么上下文
def __init__(self, subject, action, resource, context=None):
self.subject = subject
self.action = action
self.resource = resource
self.context = context or {}
class Policy:
# 一条策略:主体、动作、资源、条件共同决定效果
def __init__(self, name, effect, subjects, actions, resources, condition=None):
self.name = name
self.effect = effect # 取值只能是 Allow 或 Deny
self.subjects = subjects # 支持通配符 *
self.actions = actions
self.resources = resources
self.condition = condition # 一个函数,入参为 context 字典
def match(self, request):
if not self._hit(self.subjects, request.subject):
return False
if not self._hit(self.actions, request.action):
return False
if not self._hit(self.resources, request.resource):
return False
if self.condition and not self.condition(request.context):
return False
return True
def _hit(self, candidates, value):
# 简化处理:* 表示全部命中,否则做精确匹配
return "*" in candidates or value in candidates
class PolicyEngine:
# 策略引擎:收集所有命中的策略,按 Deny 优先原则裁决
def __init__(self):
self.policies = []
def add(self, policy):
self.policies.append(policy)
def evaluate(self, request):
matched = [p for p in self.policies if p.match(request)]
if any(p.effect == "Deny" for p in matched):
return "Deny"
if any(p.effect == "Allow" for p in matched):
return "Allow"
return "Deny" # 默认拒绝:没有策略放行就不通过接着注册两条策略并发起一次请求,验证引擎的裁决行为。
engine = PolicyEngine()
# 放行策略:客服在工作时间可以查看订单
engine.add(Policy(
name="cs-view-order",
effect="Allow",
subjects=["role:customer_service"],
actions=["order:view"],
resources=["order:*"],
condition=lambda ctx: 9 <= ctx.get("hour", 0) <= 18,
))
# 拒绝策略:任何人都不能导出客户手机号
engine.add(Policy(
name="deny-export-phone",
effect="Deny",
subjects=["*"],
actions=["order:export"],
resources=["resource:customer_phone"],
))
req = Request(
subject="role:customer_service",
action="order:view",
resource="order:1000231",
context={"hour": 14},
)
print(engine.evaluate(req)) # 输出 Allow这段实现里有几个细节值得注意。首先是默认拒绝:evaluate方法在没有策略放行时返回Deny而不是Allow,这是权限系统的安全底线,宁可错杀不可放过。其次是条件用函数表达,工程实践中会换成结构化的表达式对象,这样才能序列化入库、被可视化编辑。最后是匹配逻辑里的星号通配,真实实现通常会把资源标识拆成类型和ID两段,类型精确匹配、ID支持通配,避免全字符串匹配带来的误判。
工程落地时一般不会从零写引擎,而是直接采用成熟方案。Casbin基于PERM元模型(Policy、Effect、Request、Matcher),用配置文件描述模型、策略存数据库,和主流Web框架都有适配;OPA则更进一步,用Rego语言写策略,策略即代码,支持独立决策服务部署,在云原生领域用得很多。自研引擎的价值在于吃透机制,生产环境建议站在现成的轮子上。
落地PBAC的几个实践建议
第一条是控制策略粒度。策略太粗会退化成变相的角色权限表,失去表达能力;太细则策略数量爆炸,没人能看懂整体授权逻辑。比较务实的做法是按业务域拆分策略集,每个域内的策略数量控制在两位数,跨域的通用约束(比如全员禁止导出明文手机号)单独放一个全局策略集,优先级高于域内策略。
第二条是把策略当代码管理。策略要有唯一ID和版本号,变更走评审流程,上线前用真实的请求样本在测试环境验证命中情况。最好给每条策略补充说明文字,写清楚它服务的是哪条业务规则,半年后接手的人不至于对着一条策略猜半天。
第三条是重视决策日志。每次evaluate的结果,包括命中的策略列表、请求上下文快照、最终裁决,都应该落库。排查权限问题时,这些日志能直接回答用户为什么被拒绝;出了安全事件,它们就是审计证据。日志里注意对敏感字段脱敏,别让权限系统自己先变成泄露源。
最后说句实话,PBAC不是银弹。它带来集中管控和表达能力的同时,也引入了策略引擎这个新组件,小系统上这套架构可能是杀鸡用牛刀。但当你的权限规则开始出现时间、环境、数据范围这些维度,当权限逻辑开始散落在十几个服务的代码里时,把授权收敛成策略、交给引擎统一求值,几乎是最稳妥的演进方向。

