权限管理系统的架构设计——RBAC、ABAC 与数据权限的统一模型
一、权限问题的本质:不是一个模型,是一组正交维度
大多数团队做权限系统时,会从 RBAC(基于角色的访问控制)起步。这很自然——角色-权限的映射直观,和企业的组织架构天然对应。但随着业务复杂化,很快会遇到 RBAC 的边界:资源粒度的精细化控制、上下文敏感的临时授权、跨租户的数据隔离、字段级别的读写控制。这些需求 RBAC 不是不能做,而是会因为角色爆炸导致难以维护。
更合理的设计思路,是把权限体系拆解为功能权限和数据权限两大层,分别用不同的模型处理。功能权限回答"能不能做某个操作",适合 RBAC 或 ABAC;数据权限回答"操作的数据范围是什么",需要额外的规则引擎。两层独立演进但共享上下文,这样在复杂度增长时不会互相拖累。
实际架构设计中,我们将权限模型分为三层:认证层(AuthN,确认身份)、授权层(AuthZ,确认操作权限,RBAC+ABAC)和数据层(DataFilter,确认数据范围)。三层各司其职,通过统一的权限上下文传递决策结果。
二、统一权限架构:RBAC、ABAC 与数据过滤的协同
认证层确认用户身份后,授权层的组合决策引擎先进行 RBAC 匹配。如果角色匹配通过且不需要细粒度控制,直接放行。如果配置了 ABAC 规则(例如"仅限工作日 9:00-18:00 操作"、"仅限所属部门的数据"),则继续评估属性规则。所有规则通过后,进入数据权限层。
数据权限的筛选不是简单的 SQL 拼接,而是通过统一的拦截器在 ORM 层注入过滤条件。例如某个用户只能查看自己部门的订单,拦截器会自动注入AND department_id = :userDeptId。对于跨租户场景,拦截器注入的是租户隔离条件。对于字段级别的权限控制(例如经理可见成本字段,普通员工不可见),则在结果集返回前做字段脱敏或移除。
三、Java 实现:可组合的权限决策链
权限决策的核心抽象是责任链模式,每条规则独立的计算结果被聚合为最终决策。
public class CompositeAuthDecisionEngine { private final List<AuthRule> rules; public AuthResult evaluate(AuthContext context) { AuthResultBuilder result = AuthResultBuilder.start(context.getUserId()); for (AuthRule rule : rules) { try { RuleResult ruleResult = rule.evaluate(context); result.addRuleResult(rule.getName(), ruleResult); // 拒绝规则一票否决 if (ruleResult.getDecision() == Decision.DENY) { return result.denied(ruleResult.getReason()).build(); } // 规则执行异常时降级为拒绝,避免静默放行 if (ruleResult.getDecision() == Decision.ERROR) { return result.denied("规则 " + rule.getName() + " 执行异常: " + ruleResult.getReason()).build(); } } catch (Exception e) { // 不捕获的单条规则异常也按拒绝处理,杜绝安全漏洞 return result.denied("规则执行失败: " + e.getMessage()).build(); } } return result.allowed().build(); } }数据权限过滤通过 MyBatis 拦截器实现透明注入:
@Intercepts(@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})) public class DataScopeInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler handler = (StatementHandler) invocation.getTarget(); MetaObject metaObject = SystemMetaObject.forObject(handler); String originalSql = handler.getBoundSql().getSql(); // 从上下文获取当前用户的数据权限范围 AuthContext context = AuthContextHolder.get(); if (context == null || context.getDataScopes().isEmpty()) { return invocation.proceed(); } // 拼接数据权限过滤条件 DataScope dataScope = context.getDataScopes(); if (dataScope == null || dataScope.getCondition() == null) { return invocation.proceed(); } String filteredSql = SqlInjector.injectDataScope(originalSql, dataScope.getCondition()); // 通过反射替换 SQL,注意在生产中应使用 PreparedStatement 参数化方式 metaObject.setValue("delegate.boundSql.sql", filteredSql); return invocation.proceed(); } }四、权限缓存与性能优化
权限查询是高频操作,如果每次请求都实时加载用户的所有角色和权限,性能会受到明显影响。我们的优化策略分为三个层次:
第一层是本地缓存,使用 Caffeine 对单用户的权限计算结果缓存 5 分钟。缓存键是userId + 租户ID + 版本号,版本号确保权限变更后立即失效。
第二层是权限预加载,登录成功后将用户的所有权限一次性批量加载到缓存中,避免后续请求的逐条查询。
第三层是增量变更,通过消息队列订阅权限变更事件,精准失效受影响用户的缓存,而不是全量刷新。
缓存键的设计要特别注意安全问题:不同租户的缓存必须隔离,缓存值序列化后不应包含敏感字段(如密码哈希、个人信息)。缓存过期时间的设置需要在安全性和性能之间权衡,5 分钟是一个经验值,可根据业务安全等级调整。
五、演进方向:从静态授权到动态风险评估
RBAC 和 ABAC 解决的是"已知规则下的权限判定",但在企业内部应用中,某些操作的风险需要动态评估。例如财务审批中,金额越大风险越高;数据导出中,时间和数据量都会影响风险等级。未来权限系统的演进方向,是在静态授权之上叠加动态风险评估层,形成"规则判定 + 风险评分"的双层模型。
具体方案是在授权决策链最后增加风险评估节点,根据操作上下文(时间、地点、数据量、目标资源敏感度)计算风险分。低于阈值的直接放行,中等风险的需要二次确认(如短信验证码),高风险的直接拦截并上报安全审计。这种模式虽然没有完全替代 RBAC/ABAC,但补上了静态规则无法覆盖的盲区,适合金融、医疗等对合规要求高的行业。