
1. 数据权限到底管什么菜单权限之外的最后一道闸门先说一个我上个月接到的需求吧。项目是拿RuoYi做的后台管理系统业务方提了个诉求销售A只能看到自己名下的客户销售B的直属领导能看到本组所有人的客户市场部总监要看到全公司的客户。当时我一听这不就是典型的行级数据权限问题吗RuoYi本身已经内置了一套数据权限机制但默认配置和业务方的期望有出入所以配置修改就成了那个阶段的核心工作。这里必须先说清楚一个概念不然容易在配置的时候绕晕。RuoYi的权限体系其实是两条线菜单权限按钮权限和数据权限行级权限。菜单权限解决的是用户能看到页面上的哪个按钮、哪个Tab比如A有客户列表菜单B没有那B连页面都进不去。但数据权限解决的是另一件事——当A和B都能进入客户列表页面时A的SQL查询结果是否被自动过滤A是只看到10条还是看全100条这由数据权限决定。我在刚接触这个框架时其实把两者搞混过以为给角色分配了菜单就等于有权限看全部数据后来发现页面上数据少得可怜才意识到是数据权限这层在起作用。RuoYi把这层闸门内置到框架里确实省了不少事但难点恰恰也在这你对配置逻辑理解不到位改起来就容易出现权限放太宽或权限误伤两种极端。这篇文章就围绕RuoYi数据权限配置修改来写默认的五种策略怎么选、配置改完之后为什么不生效、执行链路到底是怎么把条件拼进SQL的、以及当默认策略不够用的时候怎么扩展。内容适用于正在用RuoYi做二次开发的Java工程师也适用于需要给业务方解释为什么数据变少了的项目负责人。2. 五套默认策略与sys_role表先搞懂配置写在哪儿、存成什么2.1 两种改配置的入口在RuoYi里修改数据权限配置有两个入口一个是后台管理界面路径是系统管理 → 角色管理 → 选择一个角色 → 数据权限另一个是直接操作数据库。大多数情况下我推荐走界面因为RuoYi的前端交互已经把你填的东西落库了不容易出错。但如果是批量调整、脚本迁移、或者要对线上数据做紧急修复直接改数据库反而更快。关键要记住的是数据权限配置是挂在角色上的不是挂在用户上的。RuoYi是经典的RBAC模型用户通过角色间接获得数据权限所以当你给某一个用户改了权限却没生效时先去看看这个用户绑定的角色是不是还有别的角色在拖后腿——多个角色并存时权限范围在某些实现里是取并集的。2.2 sys_role.data_scope的五种取值落到数据库层面核心字段就是sys_role表里的data_scope。它是一个char(1)类型的字段取值范围和含义如下data_scope值含义说明1全部数据权限不做行级过滤查出来多少就是多少2自定义数据权限手动勾选若干部门只能看这些部门的数据3本部门数据权限只能看当前用户所属部门的数据4本部门及以下数据权限看本部门以及所有下级部门的数据5仅本人数据权限只能看自己创建/归属的数据我在实际项目里见过最常用的是2和4最容易被误用的是1全部数据权限。很多团队为了图省事一开始把管理员角色的data_scope设为1结果普通运营人员也能看到全公司数据这其实是把菜单权限给了不意味着数据权限要全量放这件事给忽略了。2.3 自定义数据权限的关联表sys_role_dept如果你在界面上选了自定义数据权限系统会弹出一个部门树让你勾选勾选的结果会存到sys_role_dept表里。这张表结构很简单就是role_id和dept_id的关联关系。一个角色可以关联多个部门一个部门也可以被多个角色关联。当用户的角色是自定义数据权限时RuoYi会去查sys_role_dept把关联的部门ID列表拿出来作为后续SQL过滤的条件。这里有一个容易踩的细节如果你把角色的data_scope从2自定义改成3本部门旧的角色—部门关联记录并不会被删除只是不再被使用。我当时排查一个为什么改了配置数据还是不对的问题最后发现是两条关联数据残留导致的误判其实代码逻辑压根没走自定义分支。2.4 前端把配置写进数据库时做了什么从界面的操作路径来看角色管理页面的数据权限部分提交时会调PUT /system/role/{roleId}接口RoleServiceImpl里会执行两块操作先更新sys_role表的基础数据包括data_scope再根据角色ID删除旧的sys_role_dept并重新插入勾选的部门——前提是data_scope等于2。如果data_scope是1、3、4、5界面上根本不会出现部门树让你勾选这个交互细节也提醒了你sys_role_dept只在自定义数据权限下有实际意义。3. 从注解到SQL注入数据权限的完整执行链路3.1 注解和切面是整套机制的发动机RuoYi的数据权限不是靠SQL里手动写死在查询条件实现的而是靠一个组合拳注解DataScope 切面DataScopeAspect。你在Service或Controller方法上标注了DataScope之后切面会在方法执行前被Spring AOP拦截偷偷把过滤条件拼到你的查询SQL里。核心代码如下RuoYi自带的我简写了关键部分Aspect public class DataScopeAspect implements AspectJPrecedenceInfo { // 五种数据权限范围 public static final String DATA_SCOPE_ALL 1; public static final String DATA_SCOPE_CUSTOM 2; public static final String DATA_SCOPE_DEPT 3; public static final String DATA_SCOPE_DEPT_AND_CHILD 4; public static final String DATA_SCOPE_SELF 5; Before(annotation(dataScope)) public void doBefore(JoinPoint point, DataScope dataScope) throws Throwable { handleDataScope(point, dataScope); } // 具体处理逻辑判断当前用户、取角色、按范围拼SQL片段 }代码里的Before表示切面逻辑在方法执行前运行。这里有个知识点RuoYi把数据权限的SQL片段存到了一个ThreadLocal里去后续真正执行SQL时MyBatis的XML里会通过params.dataScope把这些片段拼接进查询语句。3.2 deptAlias和userAlias决定了SQL拼到哪个字段上注解本身带两个常用属性DataScope(deptAlias d, userAlias u)deptAlias表示查询SQL里部门表的别名userAlias表示用户表的别名。为什么要有别名因为数据权限执行时切面根本不知道你的SQL长什么样它只知道要往你写的SQL尾部加一段类似AND d.dept_id IN (...)或AND u.user_id ...的条件如果不知道别名这段条件就不知道该用哪个表名去限定。举个例子你要查一个部门下的订单列表SQL大概长这样select idselectOrderList resultTypeOrder select o.order_id, o.order_no, d.dept_id, d.dept_name from sys_order o left join sys_dept d on o.dept_id d.dept_id where if testorderNo ! null and o.order_no like concat(%, #{orderNo}, %) /if /where /select那么在Service方法上要写DataScope(deptAlias d) public ListOrder selectOrderList(Order order) { return orderMapper.selectOrderList(order); }切面拦截后会根据当前用户的角色数据权限生成类似AND d.dept_id 101的片段拼到SQL末尾。如果SQL里没有d这个别名拼接之后就会报找不到d.dept_id这是新手最容易犯的错误。3.3 五种范围分别生成什么条件理解这一步你就能预测改了配置之后数据到底变成什么样范围拼到SQL里的条件片段全部1不拼接任何条件自定义2AND d.dept_id IN (SELECT dept_id FROM sys_role_dept WHERE role_id #{roleId})本部门3AND d.dept_id #{deptId}本部门及以下4AND d.dept_id IN (SELECT dept_id FROM sys_dept WHERE dept_id #{deptId} or ancestors LIKE %仅本人5AND u.user_id #{userId}注意部门及以下的实现原理RuoYi的部门表sys_dept里有个字段叫ancestors存储了部门的所有祖先部门ID比如某部门是103它的ancestors可能是0,100,101。查本部门及以下时它实际是找到这个部门以及ancestors里包含这个部门ID的所有子部门用like来匹配常见这也是为什么部门层级一旦很深、数据量很大性能会略有下降——但大多数中小后台系统完全够用。3.4 单表和双表场景的差异切面拼接条件时还涉及一个很细节的逻辑如果注解只写了deptAlias那拼的条件用部门别名如果只写了userAlias就用用户别名如果两个都写了它会先判断当前权限范围再决定拼部门条件还是用户条件。比如范围是仅本人它会拼AND u.user_id #{userId}哪怕你写了deptAlias也没用。所以你在复制别人的方法时一定要看方法对应的SQL里到底有没有那个表和别名。我见过一个团队把所有Service方法都统一复制了DataScope(deptAlias d, userAlias u)结果其中有的SQL压根没有join用户表MyBatis执行时直接报错最后排查了很久。4. 按业务需求修改配置三种最常见的改法实操4.1 改法一把角色从本部门改成本部门及以下这个需求很常见——业务方说经理应该能看到自己部门下面所有员工的数据而不仅仅是本部门。 此时角色当前的data_scope是3需要改成4。操作步骤登录RuoYi后台进入系统管理 → 角色管理。找到目标角色点击修改。在数据权限一栏选中本部门及以下数据权限。保存。前端会调接口把data_scope字段更新为4。如果你要改的角色很多也可以直接改数据库UPDATE sys_role SET data_scope 4 WHERE role_id 102;改完之后有一个坑RuoYi用了Redis缓存角色权限直接改库不一定会立刻生效。我在一个旧版本项目里遇到过改了库重新登录后台发现数据权限还是老的。原因是权限信息已经被缓存到Redis里了key通常叫getLoginUser:{userId}之类的。解决办法就是去系统管理→监控管理→缓存监控里清理相关缓存或者在代码里引入SysPermissionService重新加载实在不行就重启一下服务这个最暴力但也最有效。4.2 改法二给自定义数据权限增加/调整部门范围如果角色是自定义数据权限想增加可查看的部门操作流程是角色管理 → 修改该角色。选择自定义数据权限。在弹出的部门树中勾选新增部门。保存。走界面保存时框架会先删除这个角色在sys_role_dept里的所有旧记录再插入最新的勾选结果。如果你在数据库里手动做要写成两条SQLDELETE FROM sys_role_dept WHERE role_id 102; INSERT INTO sys_role_dept (role_id, dept_id) VALUES (102, 101); INSERT INTO sys_role_dept (role_id, dept_id) VALUES (102, 105); INSERT INTO sys_role_dept (role_id, dept_id) VALUES (102, 108);注意多角色用户的情况。假设用户同时有角色A自定义只看101部门和角色B全部数据权限那切面在判断时是去取当前用户的所有角色集合只要其中一个角色的data_scope是1全部数据权限按照RuoYi默认的代码逻辑最后生成的条件可能是放行所有。这个细节就见仁见智了——从安全性角度看这样设计有点激进但从易用性角度看确实避免了多个角色互相打架导致什么都看不到的问题。如果你需要更细的最小权限取交集逻辑就得改DataScopeAspect里取角色范围的部分。4.3 改法三在业务代码里动态调整数据权限范围有时候你不想把数据权限做成角色管理页面的静态配置而是希望根据当前用户的一些额外属性比如项目组成员关系、地区归属动态决定他到底能看多少数据。这种场景下我一般不会去改框架默认的切面而是选择在Service方法里手动构造条件。具体做法是不依赖DataScope注解的自动拼接而是在查询前自己往传入的实体对象里塞一个特殊的params字段。RuoYi的BaseEntity里有params这个map字段MyBatis XML里可以直接通过params.dataScope引用。比如// 手动构造过滤条件片段 String dataScopeSql AND d.dept_id IN (SELECT dept_id FROM sys_role_dept WHERE role_id 102); order.setParams(Collections.singletonMap(dataScope, dataScopeSql));然后在XML里这样写select idselectOrderList resultTypeOrder select o.order_id, o.order_no, d.dept_id, d.dept_name from sys_order o left join sys_dept d on o.dept_id d.dept_id where if testparams.dataScope ! null and params.dataScope ! ${params.dataScope} /if /where /select这种方案的优点是灵活缺点是安全性要自己负责——${}是直接拼接SQL千万别把用户输入直接塞进去否则会出现注入风险。我这里只是把框架内生成的条件放进去是可以接受的。5. 扩展自定义策略当默认的五个选项不够用5.1 为什么需要扩展RuoYi自带的五种数据权限策略在绝大多数管理后台场景里是够用的。但我遇到过几个真实需求是这五种覆盖不了的只能看本人创建且未被删除的数据同时还要排除某些特定部门。按数据归属人来看但归属人不是sys_user.user_id而是另一个业务表比如customer.owner_id。按区域划分而不是按部门划分数据表里根本没有dept_id只有area_code。遇到这些情况如果你硬套默认策略会写出一堆别扭代码更合理的做法是扩展DataScopeAspect的逻辑让它可以识别你自己定义的范围值。5.2 实战扩展自定义一个仅本人及本组成员策略我分享一个实际改造过的例子。那是一个供应商协同平台每个业务员下面有若干实习生账号实习生只能看到自己的数据组长的数据组长能看到所有实习生的数据。这个逻辑用默认的五种策略怎么套都对不上因为组不是RuoYi里的部门上级也不等于部门领导。我的改造思路是修改DataScopeAspect中的常量新增一个范围值比如6表示本人及所在用户组。在sys_role表里把某个角色的data_scope手动设为6界面上下拉框没有这个选项只能改库。在切面的handleDataScope方法里新增一个分支当范围等于6时查询当前用户的组员ID列表然后拼接AND u.user_id IN (1001, 1002, 1003)。改造时的关键代码如下伪逻辑// 伪代码在DataScopeAspect新增分支 if (6.equals(role.getDataScope())) { // 调用自己的UserGroupService查出当前用户的组员ID列表 ListLong userIds userGroupService.getGroupMemberIds(user.getUserId()); String condition AND u.user_id IN ( StringUtils.join(userIds, ,) ); // 把condition塞进dataScope sql片段 }这样的好处是前端不用改菜单和按钮权限照旧后端只是在切面里加了分支其余查询逻辑完全复用。缺点是改了RuoYi的源码后升级框架时要重新合并这个取舍要提前想清楚。5.3 扩展时容易被忽略的别名问题自定义策略拼接SQL时最容易翻车的还是别名。因为你不知道调用这个方法的人将来会写什么SQL而你拼的d.dept_id、u.user_id是写死的。所以扩展策略通常会要求项目组内部约定所有带有数据权限的查询SQL部门表统一用别名d用户表统一用别名u。这个约定必须写进项目规范文档否则后续任何一个人写SQL时换了个别名你的扩展策略就在那一个方法上失效了。我通常还会在扩展时做一次防御性检查如果截获的SQL片段里包含了别名就正常拼接如果方法上没有找到对应的SQL语句至少不要让整个查询报500错误。这一点可以通过在切面里捕获异常并打印警告日志来实现。6. 我踩过的坑和最终建议6.1 数据权限不生效的排查清单如果你改了配置之后发现数据没有按预期过滤我建议按下面这个顺序排查方法上有没有加DataScope注解。这个最基础但也最容易漏。你只看XML里的SQL是看不出有没有数据权限的必须去Service或Controller方法上看有没有注解。SQL里有没有对应的别名。加了注解、但XML里没有join部门表或用户表切面拼出来的条件没有可用的列轻则条件不起作用重则SQL直接报错。当前用户是不是超管。RuoYi对admin用户和角色ID为1的角色默认不做数据权限过滤。这是框架写死的逻辑属于防呆设计。如果你拿admin账号测永远测不出过滤效果。是不是多个角色权限叠加后放行了。用户有多个角色其中一个角色范围是全部那最终结果可能就是能看到全部数据。Redis缓存没刷新。改库后权限缓存还在需要清理Redis缓存或重新登录。是否使用了不受切的内部调用。Spring AOP只拦截通过代理对象调用的方法同一个类内部this.selectXxx()这种自调用不会走切面这是Spring基础知识。6.2 让我印象最深的两个坑第一个坑是部门ID不匹配。当时有一个订单表我以为是直接用dept_id关联部门的结果查了业务表结构发现订单表里存的不是部门ID而是销售代表IDsales_rep_id。那我拼AND d.dept_id 101就没有意义因为订单表里根本没有部门ID。最后我改成了在订单表里冗余了一个dept_id字段并且在写入订单时根据销售代表的部门同步填入。这个方案业务方接受了权限实现也变简单了。第二个坑是全部数据权限太猛。我遇到过用户反馈我改了销售经理的权限为全部数据权限结果销售经理还能导出全公司财务数据。这实际上是两个问题叠加菜单权限上销售经理角色有财务报表导出按钮数据权限上刚好角色是全部数据权限。那次之后我养成了一个习惯——在给角色分配全部数据权限之前一定要先确认这个角色上挂的菜单按钮里有没有高风险操作。数据权限管的是行菜单权限管的是功能两者组合起来才是真正的权限边界。6.3 项目落地建议最后给正在用RuoYi做权限配置的团队几个建议第一权限配置的变更要走审批流尤其涉及全部数据权限和自定义数据权限时不要随随便便在数据库里改最好在后台界面操作留出操作日志。第二建议在测试环境把所有角色的data_scope组合列一张矩阵表比如角色A本部门角色B自定义会产生什么效果提前跑一遍别等上线了再让业务方当测试员。第三如果项目里数据权限逻辑比较复杂可以考虑给部门表增加一个业务属性字段比如是否允许参与数据过滤然后把DataScopeAspect的过滤逻辑改成优先读取这个字段。这样比在代码里写一堆if-else要更容易维护。第四升级RuoYi框架版本时要格外小心新版框架可能重构了DataScopeAspect的内部实现你如果改过这个类合并代码时冲突概率很高。我一般会在项目文档里单独记录所有改过的RuoYi核心类升级的时候逐个核对而不是直接覆盖。我个人在实际操作中的体会是RuoYi的数据权限功能核心价值在于它把行级权限这个通用需求做成了一个低成本接入的框架级能力。用好了业务方提的百分之八十的数据隔离需求都可以通过配置解决用不好就容易出现权限放得太宽或者权限莫名其妙不生效的问题。这中间的差距主要就取决于你对sys_role.data_scope、sys_role_dept、DataScope、DataScopeAspect这四者的理解是否到位。把这篇文章里的链路走通一遍再遇到数据权限配置修改的需求基本就能做到心里有数了。