4. 权限模型设计与落地
权限模型设计的目标不是把系统里的权限表填满,而是让“谁在什么条件下能对哪些资源做什么”这件事可表达、可审计、可收敛。落地时最常见的失败路径是:一开始按人配权限,人多了就建角色,角色多了就加继承,继承乱了就写死判断,最后没人说得清某个角色到底能访问什么。要避免这种失控,需要从业务角色出发,依次确定角色层次、权限集、资源粒度,再用权限矩阵把关系固化下来,最后用策略即代码把动态条件从代码里抽出来。
一、从业务角色到角色层次
业务角色描述的是岗位职责,例如“华东区订单审核员”“财务对账专员”“只读数据分析师”。设计时先把这些岗位按职责范围归并成少量基础角色,再按“能力叠加”建立层次,而不是按组织架构层层复制。一个可用的层次通常分三层:基础角色(如 employee,只含登录、查看本人信息)、职能角色(如 order_auditor、finance_reconciler)、管理角色(如 region_admin,在职能角色之上增加授权与审计能力)。继承关系应满足“子角色权限是父角色权限的超集”,且禁止环形继承。
角色层次的价值在于复用,但继承层数不宜超过三层。超过三层后,一次权限变更的影响面会迅速扩大,排查成本高于复用收益。建议在角色命名中直接体现层级与范围,例如 role:order_auditor:cn-east,让范围成为角色标识的一部分,而不是靠额外字段隐式表达。
二、权限集与资源粒度
权限集是“动作 + 资源类型”的组合,例如 order:read、order:approve、order:export、user:disable。动作要按业务语义拆分,不要把“导出”混进“读取”,因为导出往往涉及批量数据外带,风险等级不同。资源粒度则决定权限能细到什么程度,常见有四档:库表级、行级(如仅本区域订单)、字段级(如隐藏手机号)、记录级(如仅本人创建的工单)。粒度越细,策略越精确,但策略数量和评估开销也越高。
一个实用的取舍是:默认按“资源类型 + 行级范围”授权,敏感字段单独用字段级策略控制。例如订单审核员默认只能看本区域订单,但 customer_phone 字段需要额外 pii:read 权限才可见。这样既控制了主体范围,又控制了数据敏感度。
三、权限矩阵:把关系写成可核对的表
权限矩阵是角色与权限的交叉表,行是角色,列是权限项,单元格标记允许或拒绝。它的作用不是替代策略引擎,而是让业务方和技术方在同一张表上对齐预期。下面是一个简化示例,✓ 表示允许,— 表示不授予。
| 角色 | order:read | order:approve | order:export | pii:read | user:disable |
|---|---|---|---|---|---|
| employee | — | — | — | — | — |
| order_auditor | ✓ | ✓ | — | — | — |
| finance_reconciler | ✓ | — | ✓ | — | — |
| region_admin | ✓ | ✓ | ✓ | ✓ | ✓ |
矩阵写完后要做两件事:一是检查是否存在“权限孤岛”,即某个权限没有任何角色拥有,说明设计遗漏或该权限已废弃;二是检查是否存在“超级角色”,即某个角色拥有超过 80% 的权限项,这通常是权限蔓延的信号,需要拆分。
四、角色爆炸与权限蔓延的治理
角色爆炸的典型征兆是角色数量随组织、地区、项目线性增长,出现大量只差一个范围的重复角色。治理手段有三种:一是把“范围”从角色中剥离,作为策略参数传入,例如角色统一为 order_auditor,范围由 region 属性决定;二是用角色组合代替角色枚举,例如“审核 + 导出”由两个基础角色叠加,而不是新建一个 auditor_exporter;三是设置角色生命周期,临时角色必须带过期时间,到期自动回收。
权限蔓延则表现为权限只增不减、离职转岗后旧权限残留。治理上要建立“最小权限 + 定期复核”机制:新角色默认无权限,按需申请;每季度导出角色权限矩阵,与岗位职责比对,对超过 90 天未使用的权限自动标记待回收。下面这张图展示了从业务角色到策略执行的完整链路,以及角色爆炸和权限蔓延在链路中的位置。
五、用 OPA 实现动态授权
当授权条件涉及时间、地域、数据归属、审批状态等动态因素时,把判断写进业务代码会导致策略分散、难以审计。OPA(Open Policy Agent)把策略抽成独立的 rego 文件,应用只负责把输入(用户属性、资源属性、动作、环境)传给 OPA,由 OPA 返回允许或拒绝。下面是一个订单访问策略示例,输入包含用户角色、区域和资源归属,输出为布尔决策。
package authz
default allow = false
# 基础角色:仅允许读取本人创建的订单
allow {
input.action == "order:read"
input.user.roles[_] == "employee"
input.resource.owner == input.user.id
}
# 职能角色:允许读取和审批本区域订单
allow {
input.action == "order:approve"
input.user.roles[_] == "order_auditor"
input.resource.region == input.user.region
}
# 字段级:仅当拥有 pii:read 时才返回敏感字段
allow_pii {
input.user.roles[_] == "region_admin"
}
# 管理角色:允许禁用本区域用户,但禁止禁用自己
allow {
input.action == "user:disable"
input.user.roles[_] == "region_admin"
input.resource.region == input.user.region
input.resource.id != input.user.id
}
这段策略体现了三个关键点:一是默认拒绝,所有允许都必须显式声明;二是范围条件(region、owner)作为输入参与判断,而不是写死在角色里;三是自保护条件(不能禁用自己)直接表达为策略,避免在业务代码里补丁式拦截。策略写完后应配套单元测试,用 opa test 覆盖允许和拒绝两类用例,确保修改策略时不会意外放开权限。
整体来看,权限模型设计是一条从业务语义到执行决策的收敛链路:业务角色决定层次,层次决定权限集,权限集与资源粒度共同决定矩阵,矩阵暴露角色爆炸和权限蔓延,OPA 则把动态条件从代码中剥离并版本化。只要矩阵保持可核对、策略保持可测试,权限治理就不会退化成一张没人敢改的配置表。