若依学习笔记:数据权限
目录
4.1.1DataScopeAspect 生成 SQL 片段
前言
在若依里,菜单按钮权限解决的是“能不能点”的问题;而数据权限解决的是“点开后能看到多少行”的问题。
典型场景:
-
超级管理员打开“用户管理”——看到全公司 1000 条用户。
-
华东区主管打开“用户管理”——只看到华东区及下属 200 条用户。
-
普通员工打开“用户管理”——只能看到“自己”这一条。
如果只有菜单权限而无数据权限,那么“接口越权”和“数据泄露”几乎必然发生 。
一.基础认知
1 .1前端数据权限展示

1.2 若依数据权限 5 种范围

1.3 整体流程:

二. 表结构 & 底层逻辑
2.1 用户-角色-部门-关联表 ER
-- 用户
sys_user(user_id, dept_id, ...)
-- 角色
sys_role(role_id, data_scope, ...)
-- 用户角色
sys_user_role(user_id, role_id)
-- 角色部门(仅范围 2 用到)
sys_role_dept(role_id, dept_id)
-- 部门
sys_dept(dept_id, parent_id, ancestors, ...)
2.2 DataScopeAspect 核心链路
@Before("xxx")
public void doBefore(JoinPoint point) {
1. 拿到登录用户 → deptId、ancestors
2. 循环用户所有角色 → 决定最大范围
3. 把 SQL 片段塞进 ThreadLocal → BaseEntity.params.dataScope
}
2.3 MyBatis 接收
<sql id="dataScope">
${params.dataScope}
</sql>
<select id="selectDeptList" …>
SELECT … FROM sys_dept d
<where>
<include refid="dataScope"/>
</where>
</select>
三. 全部数据
3.1 前端“数据范围下拉框”截图

3.2 前端回显 / 提交完整代码
<el-form-item label="权限范围">
<el-select v-model="form.dataScope" @change="dataScopeSelectChange">
<el-option
v-for="item in dataScopeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
</el-form-item>
//
dataScopeOptions: [
{
value: "1",
label: "全部数据权限"
},
{
value: "2",
label: "自定数据权限"
},
{
value: "3",
label: "本部门数据权限"
},
{
value: "4",
label: "本部门及以下数据权限"
},
{
value: "5",
label: "仅本人数据权限"
}
],
3.3 后端一条龙
3.3.1 Controller
@RestController
@RequestMapping("/system/dept")
public class SysDeptController {
@Autowired
private ISysDeptService deptService;
@PreAuthorize("@ss.hasPermi('system:dept:list')")
@GetMapping("/list")
public AjaxResult list(SysDept dept) {
return success(deptService.selectDeptList(dept));
}
}
-
@PreAuthorize使用 Spring EL 调用ss(PermissionService)鉴权,确保按钮级权限。 -
入参
SysDept dept直接透传给 Service,为后续 XML 动态 SQL 提供实体包装。
3.3.2 ServiceImpl
@Service
public class SysDeptServiceImpl implements ISysDeptService {
@Autowired
private SysDeptMapper deptMapper;
@Override
@DataScope(deptAlias = "d")
public List<SysDept> selectDeptList(SysDept dept) {
return deptMapper.selectDeptList(dept);
}
}
-
@DataScope(deptAlias = "d")是整条链路的核心注解:告诉 AOP「当前查询需要数据权限,表别名是 d」。 -
注解加在 Service 而不是 Controller,可保证无论哪个 Controller 调 Service,都能自动生效,避免权限绕过。
3.3.3 Mapper
public interface SysDeptMapper {
List<SysDept> selectDeptList(@Param("dept") SysDept dept);
}
-
使用
@Param("dept")统一命名空间,防止 XML 中直接写deptName找不到参数。 -
返回
List<SysDept>而非List<Map>,保持强类型,IDE 可跳转。
3.3.4 XML
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.system.mapper.SysDeptMapper">
<sql id="dataScope">
${params.dataScope}
</sql>
<select id="selectDeptList" parameterType="SysDept" resultType="SysDept">
SELECT d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.status, d.create_time
FROM sys_dept d
<where>
d.del_flag = '0'
<if test="dept.deptName != null and dept.deptName != ''">
AND d.dept_name LIKE CONCAT('%', #{dept.deptName}, '%')
</if>
<include refid="dataScope"/>
</where>
ORDER BY d.parent_id, d.order_num
</select>
</mapper>
-
<sql id="dataScope">使用${}而非#{},意为直接拼接字符串,不会加引号,从而把 AOP 生成的整条AND (...)条件原样写进 SQL。 -
<where>标签自动处理首个AND前缀,避免条件为空时多出来的WHERE AND语法错误。 -
ORDER BY写死两级排序,保证树形表格前端展开顺序一致。
3.4 Navicat SQL 示例
-- 角色 100 配置 data_scope = 1
SELECT d.dept_id, d.dept_name
FROM sys_dept d
WHERE d.del_flag = '0'
ORDER BY d.parent_id, d.order_num;
-- 返回:全表 17 条
3.5 Console 日志(示例)
DEBUG c.r.s.m.S.selectUserList - [debug,137] - ==> Preparing: select u.user_id, u.dept_id,
u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date,
u.create_by, u.create_time, u.remark, d.dept_name, d.leader from sys_user u left join sys_dept d on u.dept_id = d.dept_id where
u.del_flag = '0' AND (d.dept_id = 105 ) LIMIT ?
四. 自定义数据
<template>
<el-dialog :title="title" :visible.sync="open" width="700px" append-to-body>
<el-form ref="form" :model="form" label-width="100px">
<el-row>
<el-col :span="24">
<el-form-item label="数据范围" prop="dataScope">
<el-select v-model="form.dataScope" placeholder="权限范围" @change="changeScope">
<el-option
v-for="dict in dict.type.sys_data_scope"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="24" v-if="form.dataScope === '2'">
<el-form-item label="数据权限" prop="deptIds">
<el-checkbox v-model="deptExpand" @change="handleCheckedTreeExpand">展开/折叠</el-checkbox>
<el-checkbox v-model="deptNodeAll" @change="handleCheckedTreeNodeAll">全选/全不选</el-checkbox>
<el-tree
ref="deptRef"
:data="deptOptions"
show-checkbox
node-key="id"
empty-text="加载中"
:props="{ label: 'label', children: 'children' }"
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
</div>
</el-dialog>
</template>
<script>
import { addRole, updateRole, getRole, deptTreeSelect } from '@/api/system/role'
export default {
name: 'Role',
dicts: ['sys_data_scope'],
data() {
return {
open: false,
title: '',
form: { roleId: undefined, roleName: '', roleKey: '', dataScope: '1', deptIds: [] },
deptOptions: [],
deptExpand: true,
deptNodeAll: false
}
},
methods: {
handleUpdate(row) {
this.reset()
const roleId = row.roleId
getRole(roleId).then(response => {
this.form = response.data
this.open = true
this.title = '修改角色'
if (this.form.dataScope === '2') {
deptTreeSelect().then(res => {
this.deptOptions = res.data
this.$refs.deptRef.setCheckedKeys(response.deptIds || [])
})
}
})
},
submitForm() {
if (this.form.dataScope === '2') {
this.form.deptIds = this.$refs.deptRef.getCheckedKeys()
}
this.$refs['form'].validate(valid => {
if (valid) {
if (this.form.roleId !== undefined) {
updateRole(this.form).then(res => this.$modal.msgSuccess('修改成功'))
}
}
})
}
}
}
</script>
-
v-if="form.dataScope === '2'":只有选择了“自定义”才渲染部门树,避免无谓渲染。 -
setCheckedKeys(response.deptIds):回显时把后端返回的已选部门 ID 列表打勾,解决“编辑后丢失”问题。 -
getCheckedKeys()提交前即时收集,防止用户手滑多选/少选。
4.1后端代码
Controller,ServiceImpl,Mapper ,XML同上。
4.1.1DataScopeAspect 生成 SQL 片段
// 范围 2 核心片段
else if (DataScopeEnum.DEPT_CUSTOM.getCode().equals(dataScope)) {
sqlString += StringUtils.format(
" AND (d.dept_id IN (SELECT dept_id FROM sys_role_dept WHERE role_id = {})) ",
role.getRoleId()
);
}
-
使用子查询而不是拼接
IN (105,106,...),防止部门过多导致 SQL 超长。 -
子查询走
sys_role_dept索引,role_id + deptId 联合主键,O(logN) 性能可控。
4.4 Navicat SQL 示例
-- 角色 101 配置 data_scope = 2,并关联 dept_id in (105,106)
SELECT d.dept_id, d.dept_name
FROM sys_dept d
WHERE d.del_flag = '0'
AND (d.dept_id IN (SELECT dept_id FROM sys_role_dept WHERE role_id = 101));
-- 返回:2 条
五. 本部门
5.1 前端代码
(同3.2)
5.2 后端差异
DataScopeAspect 片段
else if (DataScopeEnum.DEPT_SELF.getCode().equals(dataScope)) {
sqlString += StringUtils.format(" AND (d.dept_id = {}) ", user.getDeptId());
}
-
直接拿当前用户所属
deptId做等值匹配,不走子查询,性能最高。 -
只适用于“扁平型”组织,若需要子部门,请看范围 4。
5.3 Navicat SQL 示例
-- 当前用户 dept_id = 103
SELECT d.dept_id, d.dept_name
FROM sys_dept d
WHERE d.del_flag = '0'
AND (d.dept_id = 103);
-- 返回:1 条
六. 本部门及以下
6.1 后端差异
else if (DataScopeEnum.DEPT_AND_CHILD.getCode().equals(dataScope)) {
sqlString += StringUtils.format(
" AND (d.dept_id IN (SELECT dept_id FROM sys_dept WHERE dept_id = {} OR ancestors LIKE CONCAT({}, '%'))) ",
user.getDeptId(), user.getDeptId()
);
}
-
利用
ancestors字段冗余设计,一条LIKE解决所有后代查询,避免递归。 -
注意
CONCAT(?, '%')一定要传字符串,否则 MySQL 8 会隐式转换导致索引失效
6.2 Navicat SQL 示例
-- 当前用户 dept_id = 100,其 ancestors 为 0,100,
SELECT d.dept_id, d.dept_name
FROM sys_dept d
WHERE d.del_flag = '0'
AND (d.dept_id IN (
SELECT dept_id FROM sys_dept
WHERE dept_id = 100 OR ancestors LIKE CONCAT('100', '%')
));
-- 返回:3 条(100 及其子部门)
七. 仅本人
7.1 后端差异
DataScopeAspect 片段
else if (DataScopeEnum.SELF.getCode().equals(dataScope)) {
sqlString += StringUtils.format(" AND (d.create_by = '{}') ", user.getUserName());
}
-
用
create_by而非user_id,是为了兼容“后台管理员代录”场景,保证责任到人。 -
若业务需要按
user_id过滤,可自己改一行即可。
7.2 Navicat SQL 示例
-- 当前用户 user_name = 'admin'
SELECT d.dept_id, d.dept_name
FROM sys_dept d
WHERE d.del_flag = '0'
AND (d.create_by = 'admin');
-- 返回:admin 创建的部门
八.实践-车间设备管理
8.1 创建sql表

8.2 代码生成导入

具体操作详情:https://blog.csdn.net/2501_93551961/article/details/154242088?spm=1001.2014.3001.5501
8.3 权限开放
后端增加注解权限,在Service 层:

XML

设定普通用户的菜单访问权限

分配普通用户数据权限
普通用户能看到的信息
超级管理员能看到的

总结
若依的数据权限把“部门树 + 角色 + AOP + MyBatis 拼接”做成了一套可配置模板,日常业务 90% 场景无需手写 WHERE。
理解本文后,你可以:
10 分钟给任意新模块加上过滤;
快速定位“数据少了 / 多了”问题;
在二次开发中扩展出“按项目 / 按片区”等更多维度。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)