数据权限
数据权限是行级权限:控制同一接口不同角色能看到的数据范围(全部 / 本公司及以下 / 本部门及以下 / 本部门 / 仅本人 / 自定义)。它以"菜单"为锚点:开关是菜单的字段(data_scope_state)、授权按菜单存档位、代码注解按菜单 code 关联——只有开启了数据权限的菜单才参与;数据权限 Tab 展示的菜单树则由权限集合角色控制、按已授权应用进行分组(见应用权限 §4)。
1. 配置(两步)
① 菜单开启数据权限:菜单管理页编辑菜单,打开「数据权限」开关(mdc_resource_menu.data_scope_state=1)。开启时系统自动为各组织性质启用中的权限集合角色与管理员角色授予「全部数据」档(已有授权的不重复插入),防止管理员体系被"未授权=无数据"锁死;关闭开关时不删除历史授权(重新启用自动恢复),避免误清管理员已配置的数据;任何启停都会失效菜单开关缓存。

② 代码埋点:查询方法上加 @DataScope 注解(md-db-mybatis-flex):

@DataScope(code = "console:organization:user", // mdc_resource_menu.code,关联菜单的数据权限开关
orgColumn = "org_id", // 组织类档位的过滤列(不声明却授了组织档,运行时 fail fast)
userColumn = "created_by", // 「仅本人」档的过滤列(默认 created_by)
tableAliases = {...}) // 多表查询必须声明表别名映射,未声明 fail fast;
// 声明了但 SQL 未涉及这些别名时,整条静默放行(辅助查询场景)数据权限引擎总开关,需要在后端项目yml中配置 mdp.database.flex.data-scope=true。
2. 授权(六档)
角色管理 →「数据权限」Tab → 菜单树中每个可配置节点都展示一个档位下拉框(默认是"未授权"),保存时调用 POST /permission/role/saveRoleDataScope。

"档位"也就是数据范围等级(对应后台枚举类
DataScopeEnum):等级越高可见数据越多。每个(角色 × 菜单)保存一条数据到mdc_role_data_scope_rel;用户有多个角色时按优先级取最高档生效。
| 档位 | 含义 | 过滤效果 | 并集合并优先级 |
|---|---|---|---|
10 全部 | 不过滤 | 执行原始SQL,原始SQL能查询什么数据,就显示什么数据。 | 最高 |
90 自定义 | 需要开发和配置自定义 Handler 的 Bean 名 | Bean 返回的 SQL 片段,有多个时使用 OR 合并SQL | 次高 |
20 本公司及以下 | org 列匹配本公司子树 | org_id IN (SELECT id FROM mdc_org WHERE tree_path LIKE '%/{rootOrgId}/%') | — |
30 本部门及以下 | org 列匹配本部门子树 | 同上(根为部门) | — |
40 本部门 | org 列 = 当前部门 | org_id = deptId | — |
50 仅本人 | user 列 = 当前用户 | created_by = userId | 最低 |
保存为全量覆盖语义:先删该角色该批次全部授权再按提交项重建,"未授权"项不提交即等于删除授权。保存校验:菜单必须已启用数据权限;档位编码必须有效;同一菜单不允许重复授权项;90 档必须填 Bean 名(权限集合角色除外)、非 90 档不能填 Bean 名;档位不得超过同性质权限集合角色对该菜单的授权档。
用户无组织(公司/部门 id 为空)时,组织类档位条件退化为永假
id IS NULL(查不到任何数据);「仅本人」档的 userId 前置已拦截,不会为空。
自定义档位实现(二开入口):写一个 @Component 实现 DataScopeCustomHandler 返回 SQL 片段(如 t.area_id in (1,2,3)),授权时把 Bean 名填入 dataScopeImpl。
角色模板(权限集合/管理员模板)同样有「数据权限」Tab,走独立接口
POST /permission/roleTemplate/saveRoleDataScope,语义与角色授权一致。

3. 鉴权(运行时链路)
@DataScope 注解方法被调用
→ DataScopeAspect:注解参数压入 ThreadLocal(DataScopeContext),方法结束恢复外层(防嵌套污染)
→ DataScopeInterceptor(MyBatis Executor 层拦截,覆盖 BaseMapper/QueryWrapper/XML 全部查询路径):
① 菜单未启用数据权限(查 mdc_resource_menu,走缓存)→ 放行
② 取当前用户(ContextUtil:userId/companyId/deptId,部门为空回落公司)
+ 用户所有启用角色对该菜单的授权档(逐角色走缓存)
③ userId 为空(定时任务/MQ 等系统线程)→ 不过滤
④ 多角色合并:取最高优先级档;最高档为自定义时保留全部自定义授权(OR 并集)
⑤ 无任何有效授权 → 注入永假条件 id IS NULL(最小权限:未授权 = 无数据)
⑥ 含「全部」档 → 放行不改写
⑦ JSqlParser 解析 SQL,把档位条件以 AND 注入 WHERE(多表按 tableAliases 定位过滤列;
不支持 UNION,fail fast)永假条件用
id IS NULL而非1=0,是为了避免被 Druid WallFilter 拦截。拦截器内部查菜单/授权、自定义 Handler 内部查库时,会临时清空 ThreadLocal 中的注解上下文,防止自递归;SQL 解析失败同样 fail fast。
4. 示例:「用户管理」的数据权限
配置:
- 菜单管理 →「用户管理」菜单开启「数据权限」开关(系统自动给总公司权限集合与管理员角色授「全部」档);
- 用户查询方法埋点:
@DataScope(code = "console:organization:user", orgColumn = "org_id")
public Page<UserVo> page(UserQuery query) { ... }注:目前「用户管理」查询尚未实际埋点,此处为演示写法。平台中已埋点的真实示例见「系统配置」(
console:system:config,ConfigController/ConfigServiceImpl)与「消息任务」(console:message:msg,MsgTaskController)。
授权:角色管理页 →"人事专员"→ 数据权限 Tab →「用户管理」节点选择「本公司及以下」。
鉴权效果:
- 该角色用户查询用户列表 → 拦截器注入
org_id IN (SELECT id FROM mdc_org WHERE tree_path LIKE '%/{其当前公司id}/%')→ 只能查到本公司子树的用户; - 若该用户另有角色授了「仅本人」→ 合并取最高档「本公司及以下」;
- 若把授权改为"未授权" → 注入
id IS NULL→ 查不到任何数据(最小权限); - 定时任务执行同方法 → userId 为空 → 不过滤。
5. 排障速查
| 现象 | 排查 |
|---|---|
| 查询查不到数据 | 菜单 data_scope_state → 角色档位(未授权=无数据)→ @DataScope 的 orgColumn/userColumn 是否配对 → 多表是否声明 tableAliases |
| 过滤不生效 | 菜单 code 与注解 code 是否一致 → mdp.database.flex.data-scope 开关 → 是否系统线程(userId 为空不过滤) |
| 自定义档报错 | Bean 名拼写、Handler 是否为 Spring Bean、是否所有 Handler 均返回空白(单个返回空白仅被忽略不参与 OR 合并,全部为空才 fail fast 拒绝放行) |
| 档位选不了更高档 | 同性质权限集合角色对该菜单的授权档是上限 |