资讯动态

企业级中后台数据权限体系设计:从RBAC到行级SQL过滤

发布时间:2026/10/13 4:16:26 来源:尧图企业网站定制
做中后台框架的人都知道权限这东西表面看是一张表、几个接口的事可真要到企业级落地数据权限体系往往是最容易让人挠头的地方。我一直在迭代自己的一个全栈中后台框架代号“风行”定位就是给企业级内部系统提供开箱即用的底座。在我把它陆续接入真实业务验证的过程中最让我出乎意料的需求不是多租户、不是工作流而是数据权限体系的设计与实现。这套东西做得好不好直接决定了框架能不能在企业内部真正站住脚。这篇文章不打算抛开细节谈概念我会把设计思路、表结构、SQL注入方案、缓存策略、常见坑全捋一遍给正在做同类中后台系统的人一份能直接落地的参考。1. 为什么中后台必须有一套独立的数据权限体系1.1 一次让我半夜爬起来改代码的权限事故先讲一个真实到不能再真实的场景。某公司内部有个人事系统销售部门可以查看自己团队的客户订单原本以为加个“销售角色能访问订单模块”的功能权限就完事了。结果有一天一个普通销售直接在接口里改了一下订单编号发现能把别的部门同事的订单明细拉出来。问题根源不在于功能权限没做而在于列表查询接口压根没有做行级的数据过滤。换句话说系统只校验了“你能不能进这个模块”没有校验“你能看到这个模块里的哪些数据”。这个事故让我意识到一件事企业级中后台光有功能权限远远不够。功能权限回答的是“能不能访问”数据权限体系回答的则是“能访问到哪一行、哪一列”。后者往往才是企业内部真正敏感的边界。于是我在“风行”框架里从一开始就把数据权限体系作为一等公民来设计而不是等出了问题再打补丁。1.2 功能权限和数据权限之间隔着一条河很多人会把这两个概念混在一起其实它们之间的差别非常明确我通常用一个类比来解释功能权限相当于门禁卡决定你能进公司的哪几间办公室数据权限相当于办公室里的保险柜权限决定你能打开哪些抽屉、能看到哪些文件。门禁卡发得再正确如果保险柜的钥匙随便锁照样会出事。功能权限一般落在URL、按钮、菜单层面控制的是“入口”数据权限则落在数据库查询的行级别控制的是“可见范围”。举个例子同样的一个“订单列表”接口财务角色看到的是全部订单部门主管看到的是自己部门下的订单业务员只能看到自己创建的订单。接口地址一样、返回结构一样唯一不同的就是SQL语句后面追加的WHERE条件。数据权限体系要做的就是把这个追加条件的过程变得自动、可控、可审计。对比项功能权限数据权限控制粒度页面、按钮、接口行级、部分场景列级实现位置路由守卫、接口拦截SQL条件、查询执行器典型问题越权访问模块越权访问数据失效后果功能不可用数据泄露2. 数据权限体系的设计基座2.1 选型在RBAC之上再叠一层数据范围权限模型的选择有很多种ACL、RBAC、ABAC各有生存空间。我在“风行”里选择的是RBAC作为底座因为中后台系统的角色模型足够清晰而且运维同学对角色授权这件事已经很熟悉了。但RBAC天然解决不了数据范围的问题角色只能告诉你“谁能访问订单模块”不能告诉你“他能看哪几个订单”。所以我在RBAC之上又加了一层数据范围规则每个角色除了关联菜单权限、按钮权限之外还关联一条或者多条数据规则。数据规则定义了该角色在访问某类业务数据时的可见范围。这么设计的好处是不推翻原有的角色模型又能把范围控制独立出来灵活性高很多。权限判断的过程变成了两层先用RBAC判断有没有访问权再用数据规则判断能看到哪些数据。2.2 五种范围规则从全部到仅本人我把实际需求整理了一遍发现企业里的数据范围不管怎么变基本上跑不出下面这五种模式。这个分类不是我闭门造车想出来的而是从十几个真实业务场景里抽出来的共性。范围类型说明典型场景全部数据不限制范围可见所有行企业高管、系统管理员本部门及以下可见本部门和所有下级部门的数据部门主管查看团队数据仅本部门只可见当前部门的数据部门统计员仅本人只可见自己创建或分配的数据一线业务员按组织/用户自选规则允许指定若干部门或用户跨部门项目协作这五种范围基本覆盖了90%以上中后台需求。需要注意的是第五种“自选”最灵活也最容易失控所以我在实现时做了一个限制自选的范围值必须落在当前用户可见的组织树范围之内不允许把一个高层部门的名单塞给一个普通员工否则这条规则本身就成了越权漏洞。2.3 组织架构是数据权限的天然边界数据范围通常和组织架构强相关。中后台系统里几乎都有部门、岗位、人员的结构部门之间天然形成了数据的边界。比如“本部门及以下”这条规则关键点在于如何获取当前用户的所有子部门ID。很多人第一个想到的是递归查询部门少的时候无所谓但组织树一旦超过几千个节点每来一次请求就递归一次数据库会很容易被打爆。我在“风行”里没有用实时递归而是在组织表上维护了一条path字段。每个部门创建时都会生成类似1.12.34.的路径查询子部门时直接通过path LIKE 1.12.%拿到全部下级部门ID一条SQL搞定毫秒级返回。这样的设计虽然多占了一点存储空间但在数据权限这种高频率的过滤场景里收益远大于成本。3. 数据权限体系在“风行”中的具体实现3.1 核心表结构设计数据权限体系的落地离不开几张核心表。很多人一上来就想着“在用户表加一个数据范围字段”这种方案短期能用中期就会变成灾难多条规则时不知道取哪条角色一变化历史范围全部错乱。我的做法是让范围规则独立成表与角色关联。CREATE TABLE auth_data_rule ( id bigint NOT NULL AUTO_INCREMENT, role_id bigint NOT NULL COMMENT 关联的角色ID, target_module varchar(64) NOT NULL COMMENT 业务模块标识如order, scope_type tinyint NOT NULL COMMENT 1全部 2本部门及以下 3仅本部门 4仅本人 5自选, scope_values text NULL COMMENT scope_type5时存部门ID/用户ID列表, status tinyint NOT NULL DEFAULT 1, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_role_module (role_id, target_module) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT数据权限规则表;部门表那边我额外维护了一条组织路径字段CREATE TABLE sys_org ( id bigint NOT NULL AUTO_INCREMENT, parent_id bigint NOT NULL DEFAULT 0, org_name varchar(64) NOT NULL, path varchar(255) NOT NULL COMMENT 组织路径如 1.12.34., PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT组织架构表;这两张表是数据权限体系的基石业务表不需要为了权限去改结构。订单表、报销表、项目表依然是自己原来的样子权限规则只是给它们增加了一道动态过滤。3.2 请求级上下文先搞清楚“我是谁”要让权限规则在查询时生效首先要有一个贯穿整个请求周期的上下文对象。用户登录之后JWT里只存了用户ID但数据权限判断需要知道更多信息角色列表、所属部门ID、是不是超级管理员、组织路径。这些信息不可能每次都查数据库所以我在登录成功后的拦截器里提前把用户上下文加载好。type UserContext struct { UID int64 Username string DeptID int64 DeptPath string RoleIDs []int64 IsSuper bool }这个上下文对象会通过请求级的中间件注入到当前协程的上下文中后续任何服务层代码都能拿到。查询数据权限规则时就可以基于这个上下文去匹配角色对应的规则而不是临时去数据库里翻用户信息。这么做既保证了数据一致性又把权限判断的开销降到了一个很小的范围。3.3 SQL条件自动拼接不侵入业务SQL的做法数据权限体系最核心的环节就是把范围规则翻译成SQL的WHERE条件。最开始我也考虑过在业务代码里写一个工具方法每个查询逻辑手动调用。但很快发现一旦业务多了必然出现三种问题一是有人忘记调用查出越权数据二是调用顺序不一致导致结果不稳定三是规则变更时要去改每一处调用点维护成本直线上升。“风行”里的做法是做一个自治的数据权限组件业务查询只需要在Mapper层声明自己属于哪个业务模块组件会自动把过滤条件拼接到SQL上。以订单查询为例业务的原始SQL是这样的SELECT * FROM biz_order WHERE order_status ?组件在检测到当前用户不是超级管理员之后会自动追加一段条件SELECT * FROM biz_order WHERE order_status ? AND biz_order.create_by IN ( SELECT id FROM sys_user WHERE dept_id IN ( SELECT id FROM sys_org WHERE path LIKE 1.12.% ) );这个拼接动作发生在查询执行之前对上层业务完全透明。业务开发只需要知道自己的模块标识不需要关心当前用户到底是“全部范围”还是“仅本人”。这就是数据权限体系能做到开箱即用的关键。提示动态拼接SQL时一定要使用预编译参数或者严谨的框架方法严防把scope_values直接拼接进SQL字符串。数据权限体系本来是防越权的如果自己先漏了SQL注入的口子那就闹大笑话了。3.4 规则组合策略一个用户有多个角色怎么办企业系统里一个用户往往同时挂在多个角色下比如既是销售主管又是项目成员。这种情况下如果两个角色对同一个模块有不同的数据范围规则怎么取我见过简单粗暴的方案是“取并集”就是把两条规则对应的数据全都查出来。这种方案看似合理其实容易造成数据面过大更危险的是“取交集”一旦某个角色没有配置规则直接查不到数据。“风行”里的策略是优先级覆盖把规则按范围大小排一个序全部数据 本部门及以下 本部门 仅本人 自选命中更高优先级的规则就忽略当前模块的其他规则。这样可以保证一个用户即使在多个角色里也总能有一套确定的可见范围不会出现“时而有权限时而没有”的幽灵问题。4. 从零接入数据权限体系完整实操过程4.1 第一步初始化组织架构和权限规则要把数据权限体系真正用起来第一步不是写代码而是把组织树和规则数据初始化好。组织树我建议在导入旧系统数据时就同步生成每一条path不要一个一个手动补齐。初始化时需要注意父部门的path变更会波及所有子部门所以我在框架里提供了一个重建组织的接口修改完部门层级之后直接调用一次就能把所有path重新刷一遍。规则初始化相对简单可以写一个能力中心接口让运维在后台界面对指定角色、指定模块设置范围类型。不过这里有一点容易踩坑模块标识的命名必须和业务代码里一致。我在“风行”里规定模块标识统一用“领域.实体”格式例如sale.order、hr.employee、finance.receipt避免出现“订单模块”和“订单管理”这种同一模块两种叫法的混乱。4.2 第二步改造业务接口让规则生效假设现在要改造“查询订单列表”的接口让它受数据权限体系的保护。在“风行”的Mapper层只需要在查询方法上声明模块标识type OrderMapper struct { *DataPermissionInjector } func (m *OrderMapper) SelectList(cond map[string]interface{}) ([]Order, error) { sql : SELECT * FROM biz_order WHERE order_status ? // DataPermissionInjector 会在执行前自动合并当前用户的权限条件 sql, params : m.Inject(sale.order, sql, params) return runQuery(sql, params...) }这一步看似简单但有一个非常隐蔽的坑如果业务SQL本身已经包含WHERE子句且这个WHERE里已经用到了子查询或者OR条件直接追加权限条件会破坏原有SQL的逻辑。我踩过一次原SQL是WHERE order_status 1 OR order_status 2直接追加条件后变成WHERE order_status 1 OR order_status 2 AND create_by 我的ID结果被外层的OR一短路权限条件完全没起到作用。从那以后我在Inject方法里强制要求业务SQL必须以括号包裹原始条件再另起一行追加权限条件从语法层面杜绝这种遮蔽问题。4.3 第三步前端菜单和按钮的联动数据权限体系虽然主要在后端实现但前端也不能完全不管。如果后端已经过滤了数据但前端还显示着“查看其他部门数据”的按钮体验上非常奇怪。我的做法是登录后不仅返回菜单权限列表还顺带返回当前用户在各个模块的数据范围类型前端根据范围类型禁用掉某些危险的批量操作按钮。比如范围是“仅本人”的时候批量导出全部数据的按钮直接置灰避免用户点下去之后导出一个空文件或者被后端无情拒绝。前端的联动只需要做减法不需要做加法。把不该显示的入口藏起来即可真正的安全边界必须且只能在后端维护。前端不管做得多华丽都是锦上添花不是安全屏障。5. 常见问题与排查技巧实录5.1 数据权限不生效问题出在哪这是我在实际接入时遇到最多的问题。用户明明配置了“仅本人”规则查询接口却把别人的数据也带出来了。排查路径我一般按下述三步来走第一步看当前用户上下文里的角色列表是否真的包含目标角色很多“不生效”其实是用户没挂上角色JWT里的角色信息和数据库不一致第二步看模块标识是否匹配M层声明的模块标识和规则表里的target_module有没有对齐多一个空格都会导致匹配失败第三步看是不是用户命中了超级管理员分支超级管理员在很多系统里会绕过所有过滤条件这属于正常设计不是bug。还有一个经常被忽略的原因查询接口可能走的是通用查询引擎没有经过Mapper层的权限注入器。比如有些框架会把复杂查询交给一个通用的QueryBuilder此时原始SQL根本没有经历权限组件的处理等于赤裸裸地绕过了数据权限体系。遇到这种场景千万不要在QueryBuilder里再加一次过滤而是应该强制所有查询走统一入口。5.2 规则叠加之后出现数据重叠或数据空洞多个角色各有数据规则时最容易出现的是重复数据和漏数据问题。虽然我在设计里用了优先级覆盖策略但有一种情况依然会引起空洞某角色配置了“本部门及以下”而另一个角色配置了“自选”但没有配置具体部门。看起来优先级高的赢了可如果高优先级规则取不到可用范围值最终查询条件就变成一个空集合直接返回空列表。我给“风行”加了一个规则完整性校验保存规则之前自动根据规则类型检查必填项。假如是自选类型但scope_values为空则不允许保存假如是本部门及以下但用户所属组织已经是根节点则给出明确提示。配好规则之后框架还会在后台定期跑一遍规则自检脚本把有问题规则列出来而不是让用户等到查询异常的时候才察觉。5.3 性能掉坑权限子查询拖垮列表接口数据权限的过滤条件一旦做成子查询就会给列表接口带来额外的执行开销。尤其是“本部门及以下”这种涉及组织子查询的范围如果前端分页要查第500页子查询可能要执行很多次JOIN。我在一次压测中发现订单列表在未加权限过滤前响应时间500毫秒加了数据权限之后直接干到4秒检查之后发现是子查询没有命中索引。解决办法有两个。第一个是在sys_org.path字段上加前缀索引让path LIKE 1.12.%走索引第二个是把常见的范围条件映射到一张冗余的“用户可见组织快照表”每次用户登录时动态刷新查询时直接拿这张快照表JOIN性能能提升很多。快照表带来的额外代价是组织变动后需要定时重建快照我在框架里实现了一个基于版本号的重建机制每次组织变化全局递增一个版本用户在下一次请求时如果有新版本就会触发更新。5.4 超级管理员被误伤超级管理员和数据权限体系的关系非常微妙。“风行”在实现时让超级管理员无条件绕过所有数据过滤规则因为超管本身已经拥有全部权限再叠加数据规则不仅没有意义反而可能导致超管看不到部分数据排查问题的时候自己先被挡在门外。但这个绕过逻辑必须放在最顶层判断不要放到底层SQL拼接里。一旦放到底层后续如果有人想扩展“准超管”或“审计员”之类的半权限角色就会非常别扭动不动就要改核心逻辑。还有一种情况是部分用户虽然不叫超管但业务上确实需要查看全量数据比如财务总监。这个时候不要给他配“全部数据”规则然后让他绕过一切而是就走正常的规则体系配一条范围类型为“全部数据”的规则即可。这样审计的时候能查清“谁在什么时候给谁开了全量权限”避免权限被静默突破。6. 落地复盘用真金白银换来的几个心得6.1 规则配置别硬编码在注解里要让业务方能改我最初实现时为了方便把数据范围规则写在了Java式的注解上比如DataPermission(typeDataScope.SELF)。开发的时候确实很快但到了实际运营阶段就出问题了规则一改代码就要重新编译发布。企业里的权限策略调整频率远比想象中高业务方今天说销售要看全部门明天又说只看本人总不能每一次都发版吧。所以后来我把注解和数据库配置做了一个映射注解负责标记“这个模块需要被数据权限保护”但具体的范围规则全部放数据库。注解管有没有规则管怎么过滤两者彻底解耦。如果你所在项目也打算做数据权限体系我建议一开始就记住这个原则权限规则的最终控制面一定在配置中心或者数据库里代码里只保留业务模块的标记。6.2 权限缓存要按“版本”刷新而不是按“时间”刷新数据权限规则本身读多写少非常适合做缓存。但如果只做简单的过期缓存业务方改完规则后要等最长过期时间才能生效很容易被质疑“系统怎么没反应”。我给“风行”设计了一个权限版本号机制查询之前先从缓存里拿一个全局版本号规则缓存带上这个版本号作为key的一部分规则一旦修改全局版本号加一下次请求时缓存key自动失效。这样既保留了缓存带来的性能优势又让规则变更秒级生效。这套机制同时也能解决组织架构变更导致的范围缓存失效问题。部门树变了所有依赖部门路径的数据规则都受影响直接通过版本号一并刷新比逐个清理缓存键可靠得多。6.3 审计日志是数据权限体系的保命符最后必须要说的是审计日志。数据权限体系做得再严密也怕内鬼。谁在什么时间点、因为哪条角色规则、查到了哪个范围内的数据这些信息必须全部留下来。“风行”里的审计模块会把权限实际生效后的SQL条件和用户上下文一起记录平时不求快但出事时能一条条回溯。我见过太多内部数据泄露事件最后都因为缺乏权限审计而只能吃哑巴亏。具体做法上不需要记录每条查询的完整结果集那样日志量会爆炸。只需要记录查询对应的模块、规则类型、权限条件、返回行数这几个关键字段足以支撑绝大多数安全溯源场景。数据权限体系这东西初看是个技术问题实际上是把组织规则、数据边界和安全边界揉在一起的系统工程。我最大的感受是不要等出了越权事故再回头补一定要在框架层就把行级过滤做成标准能力。上面这些设计思路和踩坑记录“风行”项目里全都验证过照着做至少能让你少走大半年的弯路。如果大家在接入过程中有更刁钻的场景欢迎一起交流。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑