Files
cs-note/hhs/MS/02-服务治理/09-网关鉴权策略/RBAC权限模型实战.md
T
2026-05-24 11:42:38 +08:00

12 KiB

tags, create time
tags create time
rbac
abac
authorization
permission
access-control
opa
2026-05-17 21:35

从 RBAC 到 ABAC 的演进路径

概述

RBAC(基于角色的访问控制)是绝大多数系统的起点,但随着业务复杂度上升,单纯的角色加权限映射会逐渐显露出局限。本文带你梳理从 RBAC 起步、最终演进到 ABAC(基于属性的访问控制)的完整路径,包括什么时候该升级以及如何平滑迁移。

[!question] 什么时候 RBAC 不够用了?

假设你是某公司的财务主管,你有以下权限规则:

  • 你可以审批金额不超过 10 万的报销单
  • 你可以审批金额不超过 50 万的报销单,如果是本部门员工提交的
  • 你可以查看所有部门的财务报表,但不能修改
  • 如果你在周五下午 6 点之后尝试提交审批,会被阻止

请问:这些规则能用纯 RBAC 表达吗?

RBAC:入门最简模型

RBAC 的核心概念只有三个:用户、角色、权限。它们之间的关系如下:

graph LR
    U1["用户: 张三"] --> R1["角色: 财务经理"]
    U2["用户: 李四"] --> R2["角色: 普通员工"]
    R1 --> P1["权限: 审批报销"]
    R1 --> P2["权限: 查看部门报表"]
    R2 --> P3["权限: 提交报销单"]
    R2 --> P4["权限: 查看个人记录"]

最基础的 RBAC 判断实现方式:

func (svc *Service) HasPermission(user User, targetAction string, resourceID string) bool {
    roles := svc.roleStore.GetByUserID(user.ID)
    for _, role := range roles {
        perms := svc.permStore.GetByRoleID(role.ID)
        for _, perm := range perms {
            if perm.Action == targetAction && perm.Resource == resourceID {
                return true
            }
        }
    }
    return false
}

[!note] 解释

这段代码是最直观的 RBAC 实现:遍历用户的所有角色,查找每个角色对应的权限表。问题是它只看 action 和 resource,不看任何上下文信息——金额大小、提交人身份、时间窗口这些全部被忽略。当业务规则开始依赖这些动态属性时,RBAC 就力不从心了。

RBAC 有其明确的适用范围,也有不可忽视的局限性:

优势 代价
实现简单,一张权限表搞定一切 无法表达条件型权限比如按金额分级审批
管理直观:分配角色等于分配权限 角色爆炸——为覆盖所有组合角色数量呈指数增长
变更可控:改一个角色的权限所有成员同步更新 无法应对动态属性比如时间地域设备安全等级

[!tip] 何时 RBAC 够用?

如果你的系统满足以下条件,RBAC 就是最好的选择:

  1. 权限规则固定变化频率低
  2. 用户数量和组织结构稳定
  3. 不需要按数据内容或上下文做差异化授权

中小企业后台管理系统通常就是这类场景,用 RBAC 完全没问题。

为什么需要升级?RBAC 的瓶颈

回到开头的财务审批例子,我们来拆解每一条规则为什么 RBAC 搞不定:

规则 RBAC 能不能表达 问题分析
审批金额不超过 10 万 不行 金额不是角色或资源RBAC 的权限表中没有数值比较能力
审批本部门员工提交 不行 提交人所属部门是数据属性不在角色权限体系中
查看所有部门报表但不能修改 勉强 需要拆成查看和修改两个权限再加角色组合容易遗漏
周五下午 6 点后禁止审批 不行 时间是运行时上下文RBAC 的权限定义是静态的

根本原因是:RBAC 的权限判定只依赖用户属于哪个角色这一个维度,而其他所有因素——数据内容、时间、环境——都被视为透明。当业务需要这些因素参与决策时,就需要引入更丰富的授权模型。

过渡方案:规则引擎嵌入 RBAC

在全面升级到 ABAC 之前,可以先用规则引擎增强 RBAC 来处理一批常见的条件场景:

type ConditionalPermission struct {
    RoleID     string `json:"role_id"`
    Action     string `json:"action"`
    Condition  string `json:"condition"`
    Expression func(ctx EvalContext) bool `json:"-"`
}

func (svc *Service) CheckConditionalPerm(user User, action string, ctx EvalContext) bool {
    perms := svc.condPermStore.GetByRoleAndAction(user.Roles, action)
    for _, perm := range perms {
        if perm.Expression(ctx) {
            return true
        }
    }
    return false
}

[!note] 解释

这里的思路是在原有权限表的基础上增加一个 Condition 字段,用来存储条件表达式。CheckConditionalPerm 方法与原来的 HasPermission 类似,但它额外接收一个 EvalContext 包含运行时的上下文信息如请求金额、当前时间等。Expression 函数在上下文之上求值,判断当前条件是否满足。

这种方式是在不重构权限模型的前提下快速解决问题。但当规则越来越多时,Condition 会变得难以维护——这就是需要 ABAC 的信号。

[!question] 规则引擎和 ABAC 该怎么选?

  • 如果你只有 10 到 20 条条件规则,规则引擎完全够用
  • 当你发现有超过 50 条涉及不同数据字段的条件且经常新增时,ABAC 的结构化表达会显著降低维护成本

判断信号很简单:当你开始在文档里写如果 A 并且 B 但是 C 除外这种句子时,就该考虑迁移 ABAC 了。

ABAC:结构化属性授权

ABAC 的核心思想是:权限判定不再只看角色,而是综合评估用户属性、资源属性、环境属性和动作本身。

flowchart TB
    UA["用户属性 department level tenantId"] --> EA["评估引擎"]
    RA["资源属性 amount ownerId sensitivity"] --> EA
    En["环境属性 time ip deviceRisk"] --> EA
    Policy["授权策略 IF user.level >= 2 AND resource.amount <= 100000 THEN ALLOW"] --> EA
    EA --> Decision{"Decision Allow Deny"}
    style UA fill:#e3f2fd
    style RA fill:#fff3e0
    style En fill:#f3e5f5
    style Policy fill:#e8f5e9

上面展示了 ABAC 评估的四个输入维度。与 RBAC 的最大区别在于,每个维度都可以参与最终的决策判断,而不是仅仅作为用户的一个标签。

主流 ABAC 引擎:OPA

Open Policy Agent 是目前最流行的 ABAC 实现,使用专用语言 Rego 编写策略:

import "github.com/open-policy-agent/opa/rego"

func evaluate(opaPolicy string, input Input) (bool, error) {
    rego := rego.New(
        rego.Query("allow := data.authz.allow"),
        rego.Module("policy.rego", opaPolicy),
        rego.Input(input),
    )
    result, err := rego.Eval(context.Background())
    if err != nil {
        return false, err
    }
    return result.Allow(), nil
}

对应的 Rego 策略文件:

package authz

import input as request

allow {
    request.user.roles[_] == "finance_manager"
    request.action == "approve"
    request.resource.amount <= 100000
}

deny {
    request.action == "submit_approval"
    hour := time.hour(time.now())
    weekday := time.weekday(time.now())
    weekday == "Friday"
    hour >= 18
}

[!note] 解释

Rego 是一种声明式策略语言,每一段 allow 或 deny 规则都是一个独立的布尔表达式。上面第一条 allow 规则表示:当前提条件同时满足——用户角色包含 finance_manager、操作是 approve、资源金额不超过 10 万时,授权结果为真。deny 规则优先级更高:即使是合法用户,如果在周五晚上 6 点后提交审批也会被拒绝。

相比于 Go 代码中硬编码条件判断,Rego 策略可以独立部署和热更新,所有语言通过 HTTP 或 gRPC 调用同一个策略引擎,确保了授权决策的一致性。

RBAC 与 ABAC 的融合

不要把 RBAC 和 ABAC 看作替代品——RBAC 完全可以作为 ABAC 中的一个属性维度存在。大多数成熟系统是 RBAC 加 ABAC 的混合模式:

package authz

allow {
    some i
    request.user.roles[i] == "admin"
}

allow {
    request.user.roles[_] == "finance_manager"
    request.resource.amount <= 100000
    now := time.now_ns()
    time.ns_to_rfc3339(now) > "09:00:00Z"
    time.ns_to_rfc3339(now) < "18:00:00Z"
}

deny {
    request.user.level < 3
    request.resource.sensitivity == "top_secret"
}

[!summary] ABAC vs 嵌入式规则引擎对比

对比项 嵌入式规则引擎 OPA ABAC
策略语言 Go Python 代码 Rego 专用声明式语言
独立部署 否随业务部署 是可独立运营
多语言通用 绑定业务语言 HTTP gRPC 接口多语言共享
审计能力 需自行实现 内置策略决策日志
学习曲线 低 中高

混合模式下带来的收益包括:简单角色权限走 RBAC 分支速度快且省资源、条件型权限走 ABAC 分支灵活可扩展、Deny 优先级最高防止策略冲突导致的越权。

迁移路线图

从 RBAC 平滑升级到 RBAC 加 ABAC 的建议路径:

flowchart LR
    S1["L1 纯 RBAC"] -->|"遇到条件规则"| S2["L2 RBAC + 条件<br/>代码内嵌"]
    S2 -->|"规则超过 50 条"| S3["L3 RBAC + OPA<br/>策略独立"]
    S3 -->|"跨系统统一管理"| S4["L4 全量 ABAC<br/>多租户动态"]
    style S1 fill:#c8e6c9
    style S2 fill:#fff9c4
    style S3 fill:#ffccbc
    style S4 fill:#f8bbd0

每个阶段的特征和常见误区:

阶段 特征 建议做法 常见误区
L1 到 L2 出现简单的条件判断 在现有权限表中加 condition 字段用脚本语言评估 把所有条件写成 if-else 大函数
L2 到 L3 条件变得复杂且需要多人协同维护 引入 OPA 把策略从代码中提取为独立文件 一开始就引入 OPA杀鸡用牛刀
L3 到 L4 需要跨多个系统统一策略 构建策略管理中心通过 HTTP 或 gRPC 下发决策 放弃 RBAC其实 RBAC 还有很大价值

实战 Checklist

落地权限系统时的经验教训汇总:

项目 建议 踩过的坑
缓存 权限结果缓存 5 到 15 分钟带版本号 每次请求查 DBP99 延迟飙到 200ms 以上
默认行为 默认 Deny 明确授权才放行 默认 Allow 导致漏配策略时大面积越权
测试 用 Policy-as-Code 的思想写单元测试 上线后发现一条遗漏的规则导致业务损失
审计日志 记录每一次权限决策的输入和结果 出问题后不知道是哪个策略放的行
紧急降级 设计熔断开关授权服务故障时 fallback 到本地缓存 授权服务挂了全站请求全部被拒

总结

RBAC 到 ABAC 的演进不是替换而是扩展。一个好的权限系统应该遵循以下路径:

  1. 从简单开始:先用 RBAC 覆盖百分之八十的常规场景
  2. 渐进增强:条件规则来了就用嵌入式规则引擎接住
  3. 适时抽象:规则多了就抽离为 OPA 策略获得独立管理和多语言复用能力
  4. 永远保留 RBAC:它仍然是最简单最高效的权限表达方式不应该被 ABAC 完全取代

[!question] 读完之后想一想

  1. 你的系统中权限规则的变更频率是多少?每周几次以上就应该考虑引入独立策略引擎了?
  2. 如果授权服务完全不可用宕机加无缓存兜底你的系统会怎样?该如何设计降级策略?
  3. 在你的业务中哪些条件型权限将来可能会成为 ABAC 迁移的第一批候选?

关联笔记