配套 权限模型与RBAC-ABAC全解析 使用,涵盖 12 道面试自测题与参考答案。建议先阅读原文,再进行自测!
标签:RBAC、ABAC、ACL、权限模型、按钮级权限、数据级权限、多租户、PBAC
面试自测
| | |
|---|
| ACL 和 RBAC 的区别?为什么 RBAC 更适合企业系统? | |
| RBAC0/1/2/3 各是什么?角色继承解决什么问题? | |
| 什么是职责分离(SoD)?静态和动态职责分离的区别? | |
| RBAC 表达不了的什么场景要用 ABAC?举三个例子。 | |
| 数据级权限怎么实现?同一个接口不同人看不同数据的原理? | |
| 前端按钮级权限和后端接口级权限为什么要都做?只做前端行不行? | |
| 权限缓存怎么做?角色权限变更时如何失效?版本号机制是什么? | |
| 多租户 SaaS 的权限隔离有哪几种方案?共享表方案最易出什么 bug?跨租户应返回什么? | |
| 为什么"制单"和"审核"角色不能同一人持有?这属于 RBAC 的哪一层? | |
| 复杂合规场景为什么要上 PBAC 策略引擎?与 RBAC/ABAC 的关系? | |
| 权限编码为什么用"模块:资源:动作"三段式?有什么好处? | |
| 角色权限变更时批量清缓存有什么性能问题?如何优化? | |
参考答案
Q1:ACL 和 RBAC 的区别?为什么 RBAC 更适合企业系统?
RBAC 更适合企业的原因:
- 1. 企业人员变动频繁(入职、换岗、离职),但岗位(角色)相对固定——RBAC 让人员变动只影响"用户-角色"关联,不动角色本身
- 2. 企业按岗位划分职责,RBAC 的"角色"天然对应岗位,映射自然
- 3. 批量管理高效:所有运营人员要加新权限,改"运营角色"即可,自动对所有运营生效;ACL 要逐人加
- 4. ACL 在人员多、资源多时,每资源维护清单爆炸,维护成本不可控
ACL 的适用:文件系统、云资源 IAM、对象存储——这些"资源少、需精细到单个资源"的场景 ACL 反而合适。
参考:权限模型与RBAC-ABAC全解析 - 六、ACL 详解
Q2:RBAC0/1/2/3 各是什么?角色继承解决什么问题?
角色继承解决的问题:
"运营组长"和"运营专员"权限大量重叠——组长包含专员的所有权限,再加管理权限。无继承时要重复定义两遍专员的权限点;有继承时,组长继承专员,自动获得专员权限,只需额外定义组长独有的权限。
实现:角色表加 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:用户可同时持有"制单"和"审核"角色,但执行时检查"同一订单的制单人和审核人不能是同一人"。灵活,适合人少需要兼岗的场景。
其他约束:
- • 先决条件:必须先有"运营专员"才能授"运营组长"
参考:权限模型与RBAC-ABAC全解析 - 7.3 RBAC2 角色约束
Q4:RBAC 表达不了的什么场景要用 ABAC?举三个例子。
RBAC 表达"能不能调这个接口"很自然,但表达不了"条件性权限"——同一个权限点,在不同条件下是否允许,或同一接口不同人看不同数据。
三个 ABAC 典型场景:
- 1. 只看本部门数据:权限点
order:view 只表示"能查看订单",但"只能看本部门的订单"要靠主体属性(部门)与客体属性(订单归属部门)比较判断 - 2. 只看自己创建的:
order:view 表达不了"只能看自己创建的",要靠主体 id 与客体创建者比较 - 3. 工作时间才能访问:
order:view 表达不了"工作时间才能访问",要靠环境属性(当前时间)判断
ABAC 的四类属性:
判断逻辑:主体属性 × 客体属性 × 环境属性 → 是否允许。
关系:ABAC 不替代 RBAC,是补充——RBAC 管"能不能调接口"(粗粒度),ABAC 管"能看哪些数据/什么条件下能调"(细粒度)。多数系统 RBAC + ABAC 数据级权限就够。
参考:权限模型与RBAC-ABAC全解析 - 八、ABAC 属性鉴权详解
Q5:数据级权限怎么实现?同一个接口不同人看不同数据的原理?
数据级权限:同一个 order:view 权限,不同人看到的数据范围不同——全部、本部门、本部门及下属、仅自己。
实现原理(注解 + MyBatis 拦截器):
- 1. 定义数据范围枚举:ALL / DEPT / DEPT_AND_CHILD / SELF
- 2. 接口加注解:
@DataScope(value = Scope.DEPT) - 3. AOP 切面:拦截注解,根据当前用户和范围,构造 SQL 条件(如
dept_id = #{user.deptId}),放进 ThreadLocal - 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:前端按钮级权限和后端接口级权限为什么要都做?只做前端行不行?
不行,只做前端按钮隐藏远远不够。
前端按钮隐藏的本质:只是体验优化——让无权用户看不到按钮,不点无效按钮。但前端校验可被绕过:
后端接口校验的必要性:后端是权限的最后一道防线,必须独立校验。即使前端被绕过,后端拒绝越权请求。
@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?跨租户应返回什么?
三种隔离方案:
共享表方案最易出的 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. 可读性强:一眼看出权限含义——
user:delete 就是"删除用户",比数字 id 或随意命名清晰 - 2. 便于通配匹配:可用
user:* 表示"所有用户操作",便于按模块整体授权或排查 - 3. 便于按模块分配:分配权限时按模块筛选,"给运营角色配所有 order 相关权限"一目了然
- 4. 前后端统一:前端按钮、后端接口、菜单都用同一编码,便于对照和动态渲染
反例:
- • 数字 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. 权限模型与RBAC-ABAC全解析 — 本面试自测的配套原文
- 2. NIST RBAC 标准 (INCITS 359-2004) — RBAC 四层模型定义
- 3. OWASP Access Control Cheat Sheet — 访问控制最佳实践
- 4. Spring Security全解析 — Spring Security 权限实现
- 5. Sa-Token认证框架全解析 — Sa-Token 权限校验
- 6. 认证授权架构选型全解析 — 认证与授权的分层
- 7. OPA (Open Policy Agent) / Cedar / Drools — 策略引擎