做了这么多年后台系统,功能权限说实话没太卡过我,最头疼的一直是数据权限:同一个订单列表,销售只能看自己名下那几十条,销售主管要能看整个小组,财务又要看全公司但看不到提成明细。这套“同页面不同数据范围”的规则如果靠到处写 if-else 硬凑,短期能上线,后面每一次组织调整都会把人逼疯。构建健壮的“数据权限控制系统”,核心就是把“谁能看什么数据”从业务代码中抽离出来,用分层设计去承载规则,再通过框架集成把规则无感注入到数据访问链路里。这篇文章会从方案选型、分层设计、框架落地到线上问题排查,完整梳理一遍我的最佳实践,给正在做权限体系的后端开发、架构师一个可以直接参考的路线。
1. 先把问题聊透:数据权限到底卡在哪
1.1 功能权限和数据权限是两码事
很多团队把权限系统做成了“菜单 + 按钮”,也就是给用户分配角色,角色关联一堆资源标识,登录后前端判断某个按钮显不显示。这套 RBAC 模型解决的是“你能不能点这个按钮”,但它完全管不到“你点了之后能看到哪些数据”。
我见过不少项目,功能权限做得挺漂亮,结果数据权限就在 Service 层里用代码写死了。比如查订单列表时先拿当前用户,再判断角色,如果是普通销售就强制拼一个 user_id = 当前用户的条件,如果是主管就再拼一个 department_id IN (子部门) 的条件。刚开始只有两三个角色还好,等业务跑起来,角色变多、组织层级变深、权限维度从“本人/本组/本部门”扩展到“本人及下级/本部门及下级/指定人/仅本人”之后,这些逻辑会蔓延到几十个 Mapper 方法里。
出问题的不只是代码写得丑。审批流、定时任务、报表导出、消息推送这些场景都有数据查询,如果每个入口都手写一套过滤逻辑,漏掉一个就相当于数据越权。很多时候线上“数据泄露”根本不是被攻击,而是某个新加的后台查询接口忘了加数据权限条件,导出的 Excel 把所有客户的合同金额全带出去了。所以功能权限管的是“门禁”,数据权限管的是“房间里的每一个抽屉”,不能用门禁代替锁抽屉。
1.2 为什么不用数据库视图和前端控制
数据权限的实现思路,大多数团队绕不开这么几条路,我先说结论,再解释为什么我会选择“框架层拦截 SQL”这套方案。
前端隐藏数据这种方案基本可以直接否定。前端隐藏只是 UI 层面的操作,接口返回的完整数据依然在网络里传输,用户抓个包或者直接调接口就能看到全部,这属于安全性上的自欺欺人。数据库视图方案倒是能把行级过滤下沉到数据库,比如针对不同角色建不同的视图,但实际维护很痛苦:一张订单表关联了十几张表之后,光视图就有几十个版本,业务字段一变更就要同步改视图,而且视图里执行计划一旦因为权限条件没走对索引,DBA 会找你拼命。
真正主流的做法就两种:一种是在持久层框架里做 SQL 拦截,自动改写用户要执行的 SQL,加上强制过滤条件;另一种是引入独立的数据权限中间件,统一代理所有数据源访问。对于绝大多数中大型业务系统,前者更现实,因为它能跟现有技术栈深度融合,团队学习成本低,改动范围也容易控制。我这次的项目就是在 Spring Boot + MyBatis-Plus 这条技术栈上,用拦截器的思路来实现行级数据权限控制。
1.3 先用一张表看清需求边界
在动手设计之前,我把业务方提出的数据权限需求整理成了一张矩阵表,这也是后面做规则模型的基础:
| 角色 | 数据范围 | 规则描述 | 典型场景 |
|---|---|---|---|
| 普通销售 | 仅本人 | 创建人 = 当前用户 | 查看自己的客户和跟单记录 |
| 销售主管 | 本部门及下级部门 | 部门ID在本人管辖部门树内 | 查看团队业绩 |
| 大区经理 | 指定区域 | 区域编码 IN (华东, 华南) | 跨区域销售分析 |
| 财务专员 | 全部订单,隐藏提成字段 | 无行级限制,但脱敏部分列 | 财务核算 |
| 系统管理员 | 全部数据 | 跳过所有行级规则 | 配置维护 |
这张表看起来简单,但隐藏了一个关键问题:同一个用户在不同页面、不同操作上的数据范围可能完全不同。比如销售主管在“销售看板”能看全部门数据,在“客户编辑”只能改自己名下客户,在“导出对账单”反而只能导出自己跟进的客户。如果规则只挂在角色上,根本覆盖不了。这也是为什么我需要一套能精确到“Mapper 方法”级别的数据权限控制方案,而不是用户角色级别的粗粒度方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 分层设计:把复杂规则拆成能维护的四层架构
2.1 依赖倒置:业务代码不感知权限规则
分层设计听起来像套话,但数据权限这套逻辑如果不分层,最后一定会变成一锅粥。我之前见过一个案例,为了支持“不同的菜单入口看到不同范围的数据”,有人在 Controller 层根据菜单 ID 拼 SQL 条件,再传个 params 给 Mapper,Mapper 的 XML 里写满了 <if test="deptScope != null"> 这种动态判断。这种实现最大的问题是规则藏在业务调用链路的各个角落,想梳理清楚到底谁能看什么,得把代码从头翻到尾。
好的分层设计应该遵循依赖倒置的思路:业务代码只声明“我这个查询需要做数据权限控制”,但完全不关心规则细节;真正决定规则是“本人”还是“本部门”的逻辑,被收敛到独立的权限规则层;而把规则翻译成 SQL 条件并拼进查询的任务,交由框架拦截器去完成。业务、规则、注入三者之间通过明确定义的接口交互,谁都不依赖谁的具体实现。这套思路和嵌入式软件里的分层思想其实是相通的:上层逻辑不直接操作寄存器,而是通过驱动层接口访问硬件,目的都是降低耦合、提高可维护性。
2.2 四层职责划分
我最终把整个数
