当前位置:首页>排行榜>各种权限模型怎么选?复杂合规场景为什么要上 PBAC 策略引擎?与 RBAC/ABAC 的关系?

各种权限模型怎么选?复杂合规场景为什么要上 PBAC 策略引擎?与 RBAC/ABAC 的关系?

  • 更新时间 2026-11-11 13:33:04
各种权限模型怎么选?复杂合规场景为什么要上 PBAC 策略引擎?与 RBAC/ABAC 的关系?

配套 权限模型与RBAC-ABAC全解析 使用,涵盖 12 道面试自测题与参考答案。建议先阅读原文,再进行自测!

标签:RBAC、ABAC、ACL、权限模型、按钮级权限、数据级权限、多租户、PBAC



面试自测

#
题目
对应章节
Q1
ACL 和 RBAC 的区别?为什么 RBAC 更适合企业系统?
二、六
Q2
RBAC0/1/2/3 各是什么?角色继承解决什么问题?
七
Q3
什么是职责分离(SoD)?静态和动态职责分离的区别?
7.3
Q4
RBAC 表达不了的什么场景要用 ABAC?举三个例子。
八
Q5
数据级权限怎么实现?同一个接口不同人看不同数据的原理?
8.4
Q6
前端按钮级权限和后端接口级权限为什么要都做?只做前端行不行?
十
Q7
权限缓存怎么做?角色权限变更时如何失效?版本号机制是什么?
9.5、14.3
Q8
多租户 SaaS 的权限隔离有哪几种方案?共享表方案最易出什么 bug?跨租户应返回什么?
十一
Q9
为什么"制单"和"审核"角色不能同一人持有?这属于 RBAC 的哪一层?
7.3
Q10
复杂合规场景为什么要上 PBAC 策略引擎?与 RBAC/ABAC 的关系?
十二
Q11
权限编码为什么用"模块:资源:动作"三段式?有什么好处?
9.2
Q12
角色权限变更时批量清缓存有什么性能问题?如何优化?
14.3

参考答案


Q1:ACL 和 RBAC 的区别?为什么 RBAC 更适合企业系统?

维度
ACL
RBAC
视角
资源视角:每个资源列"谁能访问"
用户视角:用户持角色,角色对应权限
关联
用户直接关联资源
用户—角色—权限三层
维护
人员变动要逐资源调整
换角色即自动调整权限
适合
资源少、相对固定(文件系统)
用户多、角色相对固定(企业系统)

RBAC 更适合企业的原因:

  1. 1. 企业人员变动频繁(入职、换岗、离职),但岗位(角色)相对固定——RBAC 让人员变动只影响"用户-角色"关联,不动角色本身
  2. 2. 企业按岗位划分职责,RBAC 的"角色"天然对应岗位,映射自然
  3. 3. 批量管理高效:所有运营人员要加新权限,改"运营角色"即可,自动对所有运营生效;ACL 要逐人加
  4. 4. ACL 在人员多、资源多时,每资源维护清单爆炸,维护成本不可控

ACL 的适用:文件系统、云资源 IAM、对象存储——这些"资源少、需精细到单个资源"的场景 ACL 反而合适。

参考:权限模型与RBAC-ABAC全解析 - 六、ACL 详解


Q2:RBAC0/1/2/3 各是什么?角色继承解决什么问题?

层级
特性
解决问题
RBAC0
用户-角色-权限基础三层
基础授权
RBAC1
角色继承(parent_id)
角色权限重复定义
RBAC2
角色约束(职责分离等)
无法表达互斥/职责分离
RBAC3
RBAC1 + RBAC2
企业级完整模型

角色继承解决的问题:

"运营组长"和"运营专员"权限大量重叠——组长包含专员的所有权限,再加管理权限。无继承时要重复定义两遍专员的权限点;有继承时,组长继承专员,自动获得专员权限,只需额外定义组长独有的权限。

实现:角色表加 parent_id,加载权限时递归把父角色权限合并进来。

private Set<Long> expandWithParents(List<Long> roleIds) {    Set<Long> all = new HashSet<>(roleIds);    for (Long roleId : roleIds) {        Long parentId = roleMapper.findParentId(roleId);        while (parentId != null && all.add(parentId)) {            parentId = roleMapper.findParentId(parentId);        }    }    return all;}

注意事项:

  • • 继承要避免环(A 继承 B,B 又继承 A),分配前要检查
  • • 多继承会复杂化,多数系统只支持单继承
  • • 继承链不宜过深,否则权限溯源困难

参考:权限模型与RBAC-ABAC全解析 - 7.2 RBAC1 角色继承


Q3:什么是职责分离(SoD)?静态和动态职责分离的区别?

职责分离(Separation of Duties):把一个完整流程拆成多个步骤,由不同角色完成,防止单人舞弊。最典型的是财务"制单"和"审核"不能同一人——自己造单自己审批,舞弊风险极高。

静态 vs 动态职责分离:

类型
拦截时机
规则
灵活性
实现复杂度
静态 SoD
角色分配时
同一人不能同时持有互斥角色
低
简单
动态 SoD
业务执行时
同一人可持有互斥角色,但同一业务流程中不能同时扮演
高
复杂

静态 SoD:用户已持有"制单"角色,再分配"审核"角色时直接拒绝。强约束,但灵活度低——某些场景需要一人多角色(如小公司人少),静态就限制死了。

动态 SoD:用户可同时持有"制单"和"审核"角色,但执行时检查"同一订单的制单人和审核人不能是同一人"。灵活,适合人少需要兼岗的场景。

其他约束:

  • • 基数约束:一个用户最多 3 个角色
  • • 先决条件:必须先有"运营专员"才能授"运营组长"

参考:权限模型与RBAC-ABAC全解析 - 7.3 RBAC2 角色约束


Q4:RBAC 表达不了的什么场景要用 ABAC?举三个例子。

RBAC 表达"能不能调这个接口"很自然,但表达不了"条件性权限"——同一个权限点,在不同条件下是否允许,或同一接口不同人看不同数据。

三个 ABAC 典型场景:

  1. 1. 只看本部门数据:权限点 order:view 只表示"能查看订单",但"只能看本部门的订单"要靠主体属性(部门)与客体属性(订单归属部门)比较判断
  2. 2. 只看自己创建的:order:view 表达不了"只能看自己创建的",要靠主体 id 与客体创建者比较
  3. 3. 工作时间才能访问:order:view 表达不了"工作时间才能访问",要靠环境属性(当前时间)判断

ABAC 的四类属性:

属性类型
例子
主体属性
部门、职级、clearance 级别
客体属性
归属部门、密级、所有者
操作属性
读/写/删
环境属性
时间、IP、设备、地理位置

判断逻辑:主体属性 × 客体属性 × 环境属性 → 是否允许。

关系:ABAC 不替代 RBAC,是补充——RBAC 管"能不能调接口"(粗粒度),ABAC 管"能看哪些数据/什么条件下能调"(细粒度)。多数系统 RBAC + ABAC 数据级权限就够。

参考:权限模型与RBAC-ABAC全解析 - 八、ABAC 属性鉴权详解


Q5:数据级权限怎么实现?同一个接口不同人看不同数据的原理?

数据级权限:同一个 order:view 权限,不同人看到的数据范围不同——全部、本部门、本部门及下属、仅自己。

实现原理(注解 + MyBatis 拦截器):

  1. 1. 定义数据范围枚举:ALL / DEPT / DEPT_AND_CHILD / SELF
  2. 2. 接口加注解:@DataScope(value = Scope.DEPT)
  3. 3. AOP 切面:拦截注解,根据当前用户和范围,构造 SQL 条件(如 dept_id = #{user.deptId}),放进 ThreadLocal
  4. 4. MyBatis 拦截器:拦截 SQL 执行,从 ThreadLocal 取条件,自动追加到原 SQL 的 WHERE 后

关键代码:

@Aspect@Componentpublic class DataScopeAspect {    @Around("@annotation(dataScope)")    public Object apply(ProceedingJoinPoint pjp, DataScope dataScope) throws Throwable {        SessionInfo user = UserContext.get();        String condition = switch (dataScope.value()) {            case ALL -> null;            case SELF -> "create_by = " + user.getUid();            case DEPT -> "dept_id = " + user.getDeptId();            // ...        };        DataScopeContext.set(condition); // 传给 MyBatis 拦截器        return pjp.proceed();    }}

业务接口无感知:

@DataScope(value = Scope.DEPT)@GetMapping("/order/list")public List<Order> list() {    return orderMapper.selectAll(); // SQL 自动加 "where dept_id = #{user.deptId}"}

生产环境注意:SQL 改写不能简单字符串拼接(会破坏复杂 SQL),应用 JSqlParser 等 SQL 解析库解析 AST 后注入条件,避免 SQL 注入和语法破坏。

参考:权限模型与RBAC-ABAC全解析 - 8.4 数据级权限实现


Q6:前端按钮级权限和后端接口级权限为什么要都做?只做前端行不行?

不行,只做前端按钮隐藏远远不够。

前端按钮隐藏的本质:只是体验优化——让无权用户看不到按钮,不点无效按钮。但前端校验可被绕过:

  • • 攻击者用抓包工具直接构造请求,绕过前端按钮
  • • 攻击者篡改前端 JS,把隐藏的按钮改回来
  • • 攻击者直接用 curl 发请求

后端接口校验的必要性:后端是权限的最后一道防线,必须独立校验。即使前端被绕过,后端拒绝越权请求。

@RequiresPermission("user:delete")@DeleteMapping("/user/{id}")public void delete(@PathVariable Long id) { ... }

铁律:

  • • 前端权限管"显示"(用户体验)
  • • 后端权限管"安全"(越权防护)
  • • 两者都要做,以后端为准

典型事故:某系统只在前端隐藏"删除"按钮,后端接口无校验。攻击者发现接口直接可调,批量删除了竞争对手的数据。前端隐藏形同虚设。

参考:权限模型与RBAC-ABAC全解析 - 十、按钮级与接口级权限


Q7:权限缓存怎么做?角色权限变更时如何失效?版本号机制是什么?

权限缓存:用户权限加载后存 Redis,key 如 perm:user:{userId},TTL 1 小时。避免每次请求都查 5 张表。

失效场景:

变更场景
失效操作
用户角色变更
清该用户缓存
角色权限变更
清所有持有该角色的用户缓存
用户登录/登出
重新加载

最易漏的:角色权限变更时,不能只清单个用户——要找到所有持有该角色的用户,全部失效。改了角色权限却没清缓存,导致权限变更不生效(撤销了权限但用户还能用),这是常见 bug。

版本号机制(优化批量失效):

角色带版本号,缓存 key 带版本号:perm:user:{uid}:{roleVersion}。角色权限变更时,全局角色版本号 +1,老缓存 key 自然失效(不再被命中),无需逐个删除。

public Set<String> loadPermissions(Long userId) {    Long roleVersion = roleVersionService.getVersion();    String key = "perm:user:" + userId + ":" + roleVersion;    // 缓存 key 带版本,角色变更时 version+1,老缓存自动失效}

优势:变更时不必逐个清 10 万个用户的缓存 key,只需版本号 +1,避免 Redis 压力峰值。

其他优化:

  • • 消息队列异步清:批量清拆成消息异步消费
  • • 短 TTL:TTL 设短(5 分钟),容忍短暂不一致

参考:权限模型与RBAC-ABAC全解析 - 9.5 权限缓存失效策略


Q8:多租户 SaaS 的权限隔离有哪几种方案?共享表方案最易出什么 bug?跨租户应返回什么?

三种隔离方案:

方案
做法
隔离强度
成本
适用
独立数据库
每租户一个库
最强
最高
大客户、强合规
共享库独立 schema
每租户一个 schema
中
中
中型租户
共享表 + tenant_id
所有数据加 tenant_id
弱(靠代码)
最低
小租户、量大

共享表方案最易出的 bug——租户越权:查询时漏带 tenant_id 条件,导致租户 A 查到租户 B 的数据。这是 SaaS 最严重的安全事故。

防护:

  • • 用 MyBatis 拦截器统一注入 tenant_id,业务代码不手写
  • • 关键接口加测试:用租户 A 的 token 访问租户 B 的资源 id,必须返回 404
  • • 定期扫描代码,确保没有裸 SQL 漏带 tenant_id

跨租户应返回 404 而非 403:

  • • 403(无权限)会泄露"资源存在但无权访问"——攻击者据此判断资源 id 是否有效,可枚举
  • • 404(不存在)不泄露存在性——攻击者无法判断是"无权"还是"不存在"

超级管理员的特殊处理:超管需跨租户操作,走单独的"平台管理后台"(与租户后台物理分离),接口显式声明 @IgnoreTenant 跳过隔离,全程审计。

参考:权限模型与RBAC-ABAC全解析 - 十一、多租户权限隔离


Q9:为什么"制单"和"审核"角色不能同一人持有?这属于 RBAC 的哪一层?

原因:财务流程中,制单(填单据)和审核(审批单据)若由同一人完成,该人可自己造单自己审批,舞弊风险极高。职责分离(SoD)把一个完整流程拆成多步骤,由不同角色完成,互相制约防舞弊。

属于 RBAC2(角色约束):

RBAC2 在 RBAC0 基础上加角色约束,最常见的就是职责分离。具体分两种:

  • • 静态职责分离:角色分配时拦截——用户已持有"制单"角色,再分配"审核"角色直接拒绝。强约束,同人不能同时持有互斥角色
  • • 动态职责分离:业务执行时拦截——同人可同时持有两个角色,但同一订单的制单人和审核人不能是同一人。灵活,适合人少兼岗场景

静态 SoD 实现:

public void assignRole(Long userId, Long newRoleId) {    Set<Long> existingRoles = userRoleMapper.findRoleIdsByUserId(userId);    Set<Long> conflicting = roleConflictMapper.findConflicts(newRoleId);    if (!Collections.disjoint(existingRoles, conflicting)) {        throw new BusinessException("角色职责冲突,不能同时持有");    }    userRoleMapper.insert(userId, newRoleId);}

动态 SoD 实现:

public void approveOrder(Long orderId, Long userId) {    Order order = orderMapper.findById(orderId);    if (order.getCreatorId().equals(userId)) {        throw new BusinessException("制单人和审核人不能是同一人");    }    // 执行审核}

其他 RBAC2 约束:基数约束(一个用户最多 N 个角色)、先决条件(必须先有 A 角色才能授 B 角色)。

参考:权限模型与RBAC-ABAC全解析 - 7.3 RBAC2 角色约束


Q10:复杂合规场景为什么要上 PBAC 策略引擎?与 RBAC/ABAC 的关系?

上 PBAC 的原因:

场景
痛点
权限规则极复杂
时间、地点、密级、审批流、上下文组合
规则散落各处
注解 + 代码 + 配置,难维护审计
规则频繁变更
改规则要重新发版
多系统权限统一
各系统权限规则不一致

PBAC(Policy-Based Access Control) 把权限规则集中到独立策略引擎(如 OPA、Cedar、Drools),业务系统调策略引擎做决策。规则与业务代码解耦,规则变更不重新发版,集中审计。

与 RBAC/ABAC 的关系:PBAC 不是替代 RBAC/ABAC,是用策略引擎统一承载这些模型——策略里可以写 RBAC 规则(角色权限)、ABAC 规则(属性判断)、合规规则(审批流)。简单说,PBAC 是 RBAC + ABAC + 合规规则的"统一容器"。

策略示例(OPA Rego):

allow {    input.action == "view"    input.resource.type == "order"    input.subject.dept == input.resource.dept  # ABAC:本部门}allow {    input.environment.hour >= 9   # 环境属性:工作时间    input.environment.hour <= 18}

何时上 PBAC:

  • • 简单后台 RBAC:用注解 + AOP 够,不上 PBAC
  • • 中型 + 数据级权限:RBAC + ABAC 注解够,不上 PBAC
  • • 复杂合规、多系统统一、金融政企审计严:上 PBAC

关键认知:PBAC 是重型方案,简单系统用它就是过度设计。只在规则复杂到散落各处难维护时才上。

参考:权限模型与RBAC-ABAC全解析 - 十二、PBAC 策略引擎


Q11:权限编码为什么用"模块:资源:动作"三段式?有什么好处?

三段式编码:模块:资源:动作,如 user:delete、order:approve、finance:report。

好处:

  1. 1. 可读性强:一眼看出权限含义——user:delete 就是"删除用户",比数字 id 或随意命名清晰
  2. 2. 便于通配匹配:可用 user:* 表示"所有用户操作",便于按模块整体授权或排查
  3. 3. 便于按模块分配:分配权限时按模块筛选,"给运营角色配所有 order 相关权限"一目了然
  4. 4. 前后端统一:前端按钮、后端接口、菜单都用同一编码,便于对照和动态渲染
  5. 5. 避免冲突:不同模块的权限不会重名

反例:

  • • 数字 id(perm_id=1):无意义,要看表才知道是什么权限
  • • 随意命名(canDelete、isAdmin):不统一,难扩展,难按模块管理

实践:

  • • 权限编码在 sys_permission 表统一管理,接口注解引用编码
  • • 新增接口时同步新增权限点,避免"接口存在但无对应权限点"
  • • 编码一旦上线不要改(影响所有引用),要改就做兼容映射

参考:权限模型与RBAC-ABAC全解析 - 9.2 权限编码规范


Q12:角色权限变更时批量清缓存有什么性能问题?如何优化?

性能问题:角色权限变更时,要清所有持有该角色的用户缓存。若一个角色被 10 万用户持有,变更一次要清 10 万个缓存 key——Redis 瞬间承受 10 万次 DEL,可能造成 Redis 压力峰值,甚至短暂卡顿影响其他业务。

优化方案:

1. 版本号机制(推荐)

角色带版本号,缓存 key 带版本号:perm:user:{uid}:{roleVersion}。角色变更时全局版本号 +1,老缓存 key 自然不再被命中(等 TTL 过期自动清理),无需逐个删除。

public Set<String> loadPermissions(Long userId) {    Long roleVersion = roleVersionService.getVersion();    String key = "perm:user:" + userId + ":" + roleVersion;    // ...}

优势:变更时只改一个版本号,不批量清,零压力。

2. 消息队列异步清

把批量清拆成消息发到 MQ,多个消费者异步消费,避免同步阻塞和 Redis 压力集中。

3. 短 TTL

缓存 TTL 设短(如 5 分钟),容忍短暂不一致——权限变更后最多 5 分钟生效,避免主动清的复杂度。适合对实时性要求不高的场景。

4. 分批清

把 10 万用户分批(每批 1000),间隔清,避免瞬时压力。

选型:

  • • 对实时性要求高 + 用户量大:版本号机制
  • • 对实时性要求中:短 TTL + 角色变更时异步清
  • • 简单系统:直接清,量小无压力

关键认知:权限缓存的失效不只是"清不清"的问题,还有"怎么清不压垮 Redis"的问题。版本号机制是用"不主动清、靠 key 变化自然失效"的思路绕过批量删除,是大规模系统的常用手法。

参考:权限模型与RBAC-ABAC全解析 - 14.3 权限缓存的热点问题


参考资料

  1. 1. 权限模型与RBAC-ABAC全解析 — 本面试自测的配套原文
  2. 2. NIST RBAC 标准 (INCITS 359-2004) — RBAC 四层模型定义
  3. 3. OWASP Access Control Cheat Sheet — 访问控制最佳实践
  4. 4. Spring Security全解析 — Spring Security 权限实现
  5. 5. Sa-Token认证框架全解析 — Sa-Token 权限校验
  6. 6. 认证授权架构选型全解析 — 认证与授权的分层
  7. 7. OPA (Open Policy Agent) / Cedar / Drools — 策略引擎

随机文章