☰
RuoYi数据权限配置修改实战:五大策略与自定义扩展指南
2026/10/6 8:22:10 网站建设 项目流程

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 id="selectOrderList" resultType="Order"> 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 test="orderNo != null"> and o.order_no like concat('%', #{orderNo}, '%') </if> </where> </select>

那么在Service方法上要写:

@DataScope(deptAlias = "d") public List<Order> selectOrderList(Order order) { return orderMapper.selectOrderList(order); }

切面拦截后,会根据当前用户的角色数据权限,生成类似AND d.dept_id = 101的片段,拼到SQL末尾。如果SQL里没有d这个别名,拼接之后就会报"找不到d.dept_id",这是新手最容易犯的错误。

3.3 五种范围分别生成什么条件

理解这一步,你就能预测"改了配置之后数据到底变成什么样":

范围拼到SQL里的条件片段
全部(1)不拼接任何条件
自定义(2)AND d.dept_id IN (SELECT dept_id FROM sys_role_dept WHERE role_id = #{roleId})
本部门(3)AND d.dept_id = #{deptId}
本部门及以下(4)`AND d.dept_id IN (SELECT dept_id FROM sys_dept WHERE dept_id = #{deptId} or ancestors LIKE '%'
仅本人(5)AND 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。

操作步骤:

  1. 登录RuoYi后台,进入"系统管理 → 角色管理"。
  2. 找到目标角色,点击"修改"。
  3. 在"数据权限"一栏,选中"本部门及以下数据权限"。
  4. 保存。前端会调接口,把data_scope字段更新为4。

如果你要改的角色很多,也可以直接改数据库:

UPDATE sys_role SET data_scope = '4' WHERE role_id = 102;

改完之后有一个坑:RuoYi用了Redis缓存角色权限,直接改库不一定会立刻生效。我在一个旧版本项目里遇到过,改了库,重新登录后台,发现数据权限还是老的。原因是权限信息已经被缓存到Redis里了,key通常叫getLoginUser:{userId}之类的。解决办法就是去"系统管理→监控管理→缓存监控"里清理相关缓存,或者在代码里引入SysPermissionService重新加载,实在不行就重启一下服务,这个最暴力但也最有效。

4.2 改法二:给自定义数据权限增加/调整部门范围

如果角色是"自定义数据权限",想增加可查看的部门,操作流程是:

  1. 角色管理 → 修改该角色。
  2. 选择"自定义数据权限"。
  3. 在弹出的部门树中勾选新增部门。
  4. 保存。

走界面保存时,框架会先删除这个角色在sys_role_dept里的所有旧记录,再插入最新的勾选结果。如果你在数据库里手动做,要写成两条SQL:

DELETE 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 id="selectOrderList" resultType="Order"> 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 test="params.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里的部门,"上级"也不等于"部门领导"。

我的改造思路是:

  1. 修改DataScopeAspect中的常量,新增一个范围值,比如"6"表示"本人及所在用户组"。
  2. 在sys_role表里,把某个角色的data_scope手动设为6(界面上下拉框没有这个选项,只能改库)。
  3. 在切面的handleDataScope方法里,新增一个分支:当范围等于6时,查询当前用户的"组员ID列表",然后拼接AND u.user_id IN (1001, 1002, 1003)。

改造时的关键代码如下(伪逻辑):

// 伪代码:在DataScopeAspect新增分支 if ("6".equals(role.getDataScope())) { // 调用自己的UserGroupService,查出当前用户的组员ID列表 List<Long> 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 数据权限不生效的排查清单

如果你改了配置之后,发现数据没有按预期过滤,我建议按下面这个顺序排查:

  1. 方法上有没有加@DataScope注解。这个最基础,但也最容易漏。你只看XML里的SQL是看不出有没有数据权限的,必须去Service或Controller方法上看有没有注解。

  2. SQL里有没有对应的别名。加了注解、但XML里没有join部门表或用户表,切面拼出来的条件没有可用的列,轻则条件不起作用,重则SQL直接报错。

  3. 当前用户是不是超管。RuoYi对admin用户和角色ID为1的角色,默认不做数据权限过滤。这是框架写死的逻辑,属于防呆设计。如果你拿admin账号测,永远测不出过滤效果。

  4. 是不是多个角色权限叠加后"放行"了。用户有多个角色,其中一个角色范围是"全部",那最终结果可能就是能看到全部数据。

  5. Redis缓存没刷新。改库后权限缓存还在,需要清理Redis缓存或重新登录。

  6. 是否使用了不受切的内部调用。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这四者的理解是否到位。把这篇文章里的链路走通一遍,再遇到数据权限配置修改的需求,基本就能做到心里有数了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询