#若依(RuoYi)数据权限实战:从 @DataScope 到车间设备管理
一、引言在 上一课 L12 菜单权限 中我们通过PreAuthorize(ss.hasPermi(xxx))实现了接口级别的权限控制——有权限才能访问没有则返回 403。但实际业务中同一张表的数据行也需要做权限隔离。例如一个车间部门的负责人只能查看自己车间的设备数据超级管理员则可以看到所有设备。这就是数据权限——控制同一张表的不同行本质是动态修改 SQL 的 WHERE 条件。本文基于若依框架RuoYi-Vue v3.8.2以「车间设备管理」为例完整演示数据权限的实现全过程。二、数据权限五要素在若依中数据权限机制依赖以下五个组件协同工作组件位置作用DataScope注解Controller 方法上声明该方法需要数据权限过滤DataScopeAspect框架 AOP 切面类拦截DataScope拼接 SQL 过滤条件BaseEntity.paramsDomain 父类存放 DataScopeAspect 注入的 SQL 片段${params.dataScope}Mapper XML在 WHERE 子句中引用过滤 SQLsys_role.data_scope数据库角色数据权限范围1~5数据权限范围枚举值含义生成的 SQL1全部数据权限不添加过滤条件2自定义数据权限按配置的部门列表过滤3本部门数据权限AND d.dept_id 当前用户部门ID4本部门及以下AND (d.dept_id ? OR d.dept_id IN (子部门))5仅本人数据权限AND d.create_by 当前用户名三、实现步骤3.1 数据库建表设备表需要包含dept_id字段作为数据权限的过滤依据CREATETABLEworkshop_device(device_idbigint(20)NOTNULLAUTO_INCREMENTCOMMENT设备ID,device_novarchar(30)NOTNULLCOMMENT设备号,device_namevarchar(100)DEFAULTCOMMENT设备名称,device_typevarchar(50)DEFAULTCOMMENT设备类型,dept_idbigint(20)DEFAULTNULLCOMMENT所属部门ID数据权限关键字段,statuschar(1)DEFAULT0COMMENT状态,create_byvarchar(64)DEFAULTCOMMENT创建者,create_timedatetimeDEFAULTNULL,update_byvarchar(64)DEFAULT,update_timedatetimeDEFAULTNULL,remarkvarchar(500)DEFAULTNULL,PRIMARYKEY(device_id))ENGINEInnoDBDEFAULTCHARSETutf8mb4COMMENT车间设备表;-- 测试数据2 个部门各若干设备INSERTINTOworkshop_deviceVALUES(1,QianMu_001,数控机床A1,加工设备,105,0,admin,NOW(),,,NULL),(2,QianMu_002,工业机器人R2,自动化设备,105,0,admin,NOW(),,,NULL),(3,QianMu_003,3D打印机P3,打印设备,106,0,admin,NOW(),,,NULL),(4,QianMu_004,激光切割机L4,加工设备,106,0,admin,NOW(),,,NULL),(5,QianMu_005,AGV搬运车V5,物流设备,105,0,admin,NOW(),,,NULL);dept_id105→ 测试部门ry 用户所属dept_id106→ 财务部门3.2 角色数据权限配置在sys_role表中设置角色的data_scope-- 超级管理员全部数据权限UPDATEsys_roleSETdata_scope1WHERErole_id1;-- 普通角色本部门数据权限UPDATEsys_roleSETdata_scope4WHERErole_id2;3.3 Domain 实体类关键继承BaseEntity包含deptId字段。publicclassWorkshopDeviceextendsBaseEntity{privateLongdeviceId;privateStringdeviceNo;privateStringdeviceName;privateStringdeviceType;privateLongdeptId;// ← 数据权限过滤字段privateStringstatus;// ... getters/setters}思考为什么必须继承 BaseEntity因为BaseEntity中有MapString, Object params属性DataScopeAspect 会把拼接好的 SQL 片段存入params.dataScopeMapper XML 通过${params.dataScope}引用。3.4 Mapper XML — 数据权限的关键这是整个机制最关键的一行${params.dataScope}selectidselectWorkshopDeviceListparameterTypeWorkshopDeviceresultMapWorkshopDeviceResultSELECT d.device_id, d.device_name, d.device_no, d.device_type, d.dept_id, d.status, d.create_time FROM workshop_device dwhereiftestdeviceName ! null and deviceName ! AND d.device_name LIKE CONCAT(%, #{deviceName}, %)/ififtestdeviceType ! null and deviceType ! AND d.device_type #{deviceType}/if${params.dataScope}!-- ★ 数据权限过滤 SQL 片段 --/whereORDER BY d.device_id/select⚠️ 注意必须用${}而不是#{}${}是字符串替换会直接把 SQL 片段拼接到语句中#{}是参数占位符会把它当作字符串值处理导致语法错误。这也是 MyBatis 中少数必须用${}的场景之一另一个是动态排序ORDER BY ${column}。3.5 Controller — 加上 DataScope 注解RestControllerRequestMapping(/system/device)publicclassWorkshopDeviceControllerextendsBaseController{AutowiredprivateIWorkshopDeviceServiceworkshopDeviceService;/** * 查询车间设备列表 */PreAuthorize(ss.hasPermi(system:device:list))DataScope(deptAliasd)// ← 声明数据权限表别名为 dGetMapping(/list)publicTableDataInfolist(WorkshopDevicedevice){startPage();ListWorkshopDevicelistworkshopDeviceService.selectWorkshopDeviceList(device);returngetDataTable(list);}// ... 其他 CRUD 方法}deptAlias d对应 Mapper XML 中workshop_device d的别名DataScope和PreAuthorize可以共存前者控行后者控接口四、源码分析DataScopeAspect 做了什么DataScopeAspect是若依框架中的一个 AOP 切面类位于ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/DataScopeAspect.java。4.1 核心流程Controller 方法被调用 → DataScope 触发 AOP 拦截 → DataScopeAspect.doBefore() → clearDataScope() // 清空 params → handleDataScope() // 根据角色 data_scope 拼接 SQL → dataScopeFilter() // 最终 SQL 组装 → params.dataScope 被赋值为 SQL 片段 → Service → Mapper 执行 SQL → ${params.dataScope} 被替换为 WHERE 条件4.2 四种权限的 SQL 生成以本文的车间设备为例DataScope(deptAlias d)权限范围生成的 SQL 片段全部数据权限空字符串不过滤自定义数据权限AND d.dept_id IN (105, 106)本部门数据权限AND d.dept_id 105本部门及以下AND (d.dept_id 105 OR d.dept_id IN (SELECT dept_id FROM sys_dept WHERE FIND_IN_SET(105, ancestors)))仅本人数据权限AND d.create_by ry4.3 dataScopeFilter 方法关键代码privatevoiddataScopeFilter(JoinPointjoinPoint,SysUseruser,StringdeptAlias,StringuserAlias){StringBuildersqlStringnewStringBuilder();for(SysRolerole:user.getRoles()){StringdataScoperole.getDataScope();if(1.equals(dataScope)){// 全部数据权限 → 不加条件sqlStringnewStringBuilder();break;}elseif(2.equals(dataScope)){// 自定义 → 按配置的部门列表sqlString.append(StringUtils.format( OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id {} ),deptAlias,role.getRoleId()));}elseif(3.equals(dataScope)){// 本部门sqlString.append(StringUtils.format( OR {}.dept_id {},deptAlias,user.getDeptId()));}elseif(4.equals(dataScope)){// 本部门及以下sqlString.append(StringUtils.format( OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id {} or FIND_IN_SET({}, ancestors) ),deptAlias,user.getDeptId(),user.getDeptId()));}elseif(5.equals(dataScope)){// 仅本人sqlString.append(StringUtils.format( OR {}.create_by {},userAlias,user.getUserName()));}}if(StringUtils.isNotBlank(sqlString.toString())){// 去掉最前面的 OR变成 AND (...)ObjectparamsjoinPoint.getArgs()[0];if(paramsinstanceofBaseEntity){BaseEntitybaseEntity(BaseEntity)params;baseEntity.getParams().put(DATA_SCOPE, AND (sqlString.substring(4)));}// ↑ 最终 params.dataScope AND (d.dept_id 105)}}五、测试验证测试环境用户角色所属部门数据权限范围admin超级管理员研发部门(103)1 - 全部ry普通角色测试部门(105)4 - 本部门测试结果admin 登录全部数据权限GET /system/device/list 返回 5 条数据 QianMu_001 数控机床A1 dept_id105 QianMu_002 工业机器人R2 dept_id105 QianMu_003 3D打印机P3 dept_id106 QianMu_004 激光切割机L4 dept_id106 QianMu_005 AGV搬运车V5 dept_id105ry 登录本部门数据权限GET /system/device/list 返回 3 条数据 QianMu_001 数控机床A1 dept_id105 QianMu_002 工业机器人R2 dept_id105 QianMu_005 AGV搬运车V5 dept_id105✅ ry 无法看到 dept_id106 的QianMu_003和QianMu_004后端日志对比admin 执行的 SQL无数据权限过滤SELECTd.device_id,d.device_name,d.device_no,d.device_type,d.dept_id,d.status,d.create_timeFROMworkshop_device dORDERBYd.device_idry 执行的 SQL被 DataScopeAspect 注入条件SELECTd.device_id,d.device_name,d.device_no,d.device_type,d.dept_id,d.status,d.create_timeFROMworkshop_device dWHERE(d.dept_id105)← 自动添加的过滤条件ORDERBYd.device_id六、总结数据权限 vs 菜单权限维度L12 菜单权限L13 数据权限控制粒度接口级别数据行级别注解PreAuthorizeDataScope拦截方式Spring SecurityAOP 切面本质阻止请求进入方法动态修改 SQL常见场景菜单显隐、接口访问不同部门看不同数据共同点本质都是修改 SQL 语句——L12 的分页是通过LIMIT限制返回行数L13 的数据权限是通过WHERE过滤数据行。关键点回顾Domain 必须继承 BaseEntity— 因为需要params属性传递 SQL 片段Mapper XML 用${params.dataScope}— 不能用#{}必须是字符串替换表别名要一致—DataScope(deptAlias d)与 SQL 中的workshop_device d对应部门字段命名为 dept_id— 若依的 DataScopeAspect 默认使用dept_id列名角色 data_scope 配置在 sys_role 表— 5 种权限范围通过这个字段区分拓展思考同一张表的不同列如何进行权限控制比如用户 A 看不到手机号码列超级管理员可以看到。思路控制列的显示本质是SELECT子句的控制——可以在 Mapper 层根据用户权限动态选择查询的列集合或者在前端根据权限指令类似v-hasPermi控制列的显隐。