数据权限
数据权限是行级权限:控制同一接口不同角色能看到的数据范围(全部 / 本公司及以下 / 本部门及以下 / 本部门 / 仅本人 / 自定义)。它以"菜单"为锚点:开关是菜单的字段(data_scope_state)、授权按菜单存档位、代码注解按菜单 code 关联——只有开启了数据权限的菜单才参与;数据权限 Tab 展示的菜单树则由权限集合角色控制、按已授权应用进行分组(见角色管理 §5.1)。
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", // 组织类档位的过滤列(不声明则不支持组织档)
userColumn = "created_by", // 「仅本人」档的过滤列(默认 created_by)
tableAliases = {...}) // 多表查询必须声明表别名映射,未声明 fail fast数据权限引擎总开关为配置 mdp.database.flex.data-scope=true。
2. 授权(六档)
角色管理页 →「数据权限」Tab → 菜单树中每个可配置节点都展示一个档位下拉(首项"未授权"),保存时调用 POST /permission/role/saveRoleDataScope。
"档位"即数据范围等级(对应
DataScopeEnum):等级越高可见数据越多。每个(角色 × 菜单)保存一个档位到mdc_role_data_scope_rel.dataScope;用户有多个角色时按优先级取最高档生效。
| 档位 | 含义 | 过滤效果 | 并集合并优先级 |
|---|---|---|---|
10 全部 | 不过滤 | 不改写 SQL | 最高 |
90 自定义 | 填 dataScopeImpl(自定义 Handler 的 Bean 名) | Bean 返回的 SQL 片段,多个 OR 合并 | 次高 |
20 本公司及以下 | org 列匹配本公司子树 | org列 IN (SELECT id FROM mdc_org WHERE tree_path LIKE '%/{rootOrgId}/%') | — |
30 本部门及以下 | org 列匹配本部门子树 | 同上(根为部门) | — |
40 本部门 | org 列 = 当前部门 | org列 = deptId | — |
50 仅本人 | user 列 = 当前用户 | userColumn = userId | 最低 |
保存校验:菜单必须已启用数据权限;90 档必须填 Bean 名(权限集合角色除外);档位不得超过同性质权限集合角色对该菜单的授权档。
自定义档位实现(二开入口):写一个 @Component 实现 DataScopeCustomHandler 返回 SQL 片段(如 t.area_id in (1,2,3)),授权时把 Bean 名填入 dataScopeImpl。
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 拦截。
4. 示例:「用户管理」的数据权限
配置:
- 菜单管理 →「用户管理」菜单开启「数据权限」开关(系统自动给总公司权限集合与管理员角色授「全部」档);
- 用户查询方法埋点:
@DataScope(code = "console:organization:user", orgColumn = "org_id")
public Page<UserVo> page(UserQuery query) { ... }授权:角色管理页 →"人事专员"→ 数据权限 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、返回片段是否为空(空则 fail fast 拒绝放行) |
| 档位选不了更高档 | 同性质权限集合角色对该菜单的授权档是上限 |