目录

前言

一.基础认知

1 .1前端数据权限展示

1.2 若依数据权限 5 种范围

1.3 整体流程:

二. 表结构 & 底层逻辑

2.1 用户-角色-部门-关联表 ER

2.2 DataScopeAspect 核心链路

2.3 MyBatis 接收

三. 全部数据

3.1 前端“数据范围下拉框”截图

3.2 前端回显 / 提交完整代码

3.3 后端一条龙

3.3.1 Controller

3.3.2 ServiceImpl

3.3.3 Mapper

3.3.4 XML

3.4 Navicat SQL 示例

3.5 Console 日志(示例)

四. 自定义数据

4.1后端代码

4.1.1DataScopeAspect 生成 SQL 片段

4.4 Navicat SQL 示例

五. 本部门

5.1 前端代码

5.2 后端差异

5.3 Navicat SQL 示例

六. 本部门及以下

6.1 后端差异

6.2 Navicat SQL 示例

七. 仅本人

7.1 后端差异

7.2 Navicat SQL 示例

八.实践-车间设备管理

8.1 创建sql表

8.2 代码生成导入

8.3 权限开放

总结


前言

在若依里,菜单按钮权限解决的是“能不能点”的问题;而数据权限解决的是“点开后能看到多少行”的问题。
典型场景:

  • 超级管理员打开“用户管理”——看到全公司 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 分钟给任意新模块加上过滤;
快速定位“数据少了 / 多了”问题;
在二次开发中扩展出“按项目 / 按片区”等更多维度。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐