广告位 · 页头(后台-模块管理-新建模块后替换此区域)

什么是PBAC基于策略访问控制?和RBAC、ABAC有什么区别?

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

什么是PBAC基于策略访问控制?和RBAC、ABAC有什么区别?

PBAC的核心思想:把权限判断收敛为策略求值

PBAC的出发点很朴素:一次权限判断本质上是一次决策,决策需要输入(谁、要对什么资源、做什么操作、处在什么环境),也需要输出(允许或拒绝)。既然如此,不如把决策依据显式地写成一条条策略,每条策略描述在什么条件下、对什么主体、对什么资源上的什么动作,应该产生什么效果。运行时由策略引擎统一加载策略集,对每次请求做匹配和求值,业务代码只管拿结论。

这个模式里有两个关键角色需要认识。一个是PEP(Policy Enforcement Point,策略执行点),部署在业务链路里,负责拦截请求、组装上下文、调用决策服务并强制执行结果;另一个是PDP(Policy Decision Point,策略决策点),也就是策略引擎本身,只负责根据策略集给出结论,不掺和业务细节。两者分离之后,业务代码里不再出现具体的权限判断,只剩一个统一的拦截调用,权限规则调整也不再需要改代码发版。

这种收敛带来的好处是结构性的。策略可以独立做版本管理,像代码一样评审、测试、灰度上线;每次决策天然形成审计日志,合规检查有据可查;当规则需要跨系统复用时,只需要让不同系统的PEP对接同一个PDP,而不是各自复制一套判断逻辑再各自修bug。

PBAC、RBAC、ABAC放在一起看才清楚

不少资料把PBAC和RBAC、ABAC并列成三种互斥模型,这其实是个常见误区。更准确的理解是:RBAC和ABAC回答的是用什么要素做决策(角色或属性),PBAC回答的是用什么形式组织和管理这些决策依据(策略)。一条PBAC策略的匹配条件里完全可以引用角色,也可以引用属性,所以PBAC更像一个框架和容器,能把前两者的能力都装进来。三者的直观对比如下:

对比维度RBACABACPBAC
授权载体角色属性与关系策略
表达能力静态、偏粗粒度动态、细粒度由策略内容决定,可粗可细
规则可读性直观规则分散、排查困难策略集中、可审计
典型实现数据库角色权限表XACMLOPA、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不是银弹。它带来集中管控和表达能力的同时,也引入了策略引擎这个新组件,小系统上这套架构可能是杀鸡用牛刀。但当你的权限规则开始出现时间、环境、数据范围这些维度,当权限逻辑开始散落在十几个服务的代码里时,把授权收敛成策略、交给引擎统一求值,几乎是最稳妥的演进方向。

广告位 · 文章底部(后台-模块管理替换此区域)