从源码对比若依与 Geek-XD:为什么基于 MyBatis-Flex 方言的数据权限更可靠?
摘要
在后台管理系统中,数据权限不是简单的“列表查询过滤”。真正的问题往往出现在按 ID 查询、编辑、删除、批量操作等场景中:用户在列表里看不到某条数据,但如果知道 ID,是否还能直接操作?
本文基于若依 RuoYi-Vue 与 Geek-XD 的源码,对两者的数据权限实现进行对比。若依采用 @DataScope + BaseEntity.params + XML 占位 的方式实现数据权限;Geek-XD 则基于 MyBatis-Flex 方言扩展,将数据权限条件注入到 QueryWrapper/SQL 生成阶段。
从工程可靠性角度看,Geek-XD 的方案更靠近 ORM 层统一治理,能够降低业务代码遗漏权限校验的概率。
目录
一、数据权限真正要解决什么问题
在后台系统里,菜单权限、按钮权限、接口权限通常比较容易理解:
- 用户有没有进入某个菜单的权限;
- 用户有没有点击新增、修改、删除按钮的权限;
- 用户有没有访问某个接口的权限。
但数据权限更细。
它关注的是:同一个接口、同一张表、同一类业务数据,不同用户能看到和操作的数据范围是否不同。
例如:
- 总部管理员可以查看所有部门用户;
- 部门经理只能查看本部门及下级部门用户;
- 普通员工只能查看自己相关的数据;
- 自定义角色只能查看被授权的部门数据。
如果数据权限只在列表查询时生效,而删除、修改、详情接口没有同样的约束,就会出现一个典型风险:
页面列表里看不到的数据,可能通过 ID 直接操作。
所以,一个更可靠的数据权限方案,不应该只关注列表查询,还应该尽量覆盖按 ID 查询、编辑、删除、批量操作等场景。
二、若依的数据权限实现方式
若依的数据权限核心由两部分组成:
@DataScope注解;DataScopeAspect切面。
典型用法如下:
@DataScope(deptAlias = "d", userAlias = "u")
public List<SysUser> selectUserList(SysUser user) {
return userMapper.selectUserList(user);
}
切面会在方法执行前,根据当前登录用户、角色、部门范围拼接一段 SQL 条件。
简化后的切面入口如下:
@Before("@annotation(controllerDataScope)")
public void doBefore(JoinPoint point, DataScope controllerDataScope) {
clearDataScope(point);
handleDataScope(point, controllerDataScope);
}
真正写入数据权限 SQL 的地方,是方法第一个参数:
Object params = joinPoint.getArgs()[0];
if (StringUtils.isNotNull(params) && params instanceof BaseEntity) {
BaseEntity baseEntity = (BaseEntity) params;
baseEntity.getParams().put(
DATA_SCOPE,
" AND (" + sqlString.substring(4) + ")"
);
}
也就是说,若依的数据权限 SQL 最终被放到了:
BaseEntity.params.dataScope
BaseEntity 中提供了 params 字段,用于承载这类扩展参数:
public class BaseEntity implements Serializable {
private Map<String, Object> params;
public Map<String, Object> getParams() {
if (params == null) {
params = new HashMap<>();
}
return params;
}
}
随后,MyBatis XML 中需要手动写入 ${params.dataScope}:
<select id="selectUserList" parameterType="SysUser" resultMap="SysUserResult">
select u.user_id, u.dept_id, u.user_name, d.dept_name
from sys_user u
left join sys_dept d on u.dept_id = d.dept_id
where u.del_flag = '0'
<!-- 数据范围过滤 -->
${params.dataScope}
</select>
这个方案的链路可以概括为:
@DataScope
-> DataScopeAspect 拼接 SQL
-> 写入第一个参数的 BaseEntity.params.dataScope
-> XML 中通过 ${params.dataScope} 拼接
-> 最终 SQL 生效
它的优点是简单、直观,适合传统 MyBatis XML 项目。
但它也有明显的结构性限制。
三、若依方案的边界问题
若依的数据权限要自然生效,通常需要同时满足几个条件:
- 方法上加了
@DataScope; - 方法第一个参数是
BaseEntity子类; - XML SQL 中写了
${params.dataScope}; - 业务调用路径确实经过了这个带注解的方法。
在列表查询场景中,这通常没有问题。
例如:
@DataScope(deptAlias = "d", userAlias = "u")
public List<SysUser> selectUserList(SysUser user) {
return userMapper.selectUserList(user);
}
因为 SysUser 继承了 BaseEntity,可以承载 params.dataScope。
但是对于按 ID 删除,问题就出现了。
例如:
public int deleteUserById(Long userId) {
return userMapper.deleteUserById(userId);
}
这里的参数是 Long,不是 BaseEntity。
即使方法上加了 @DataScope,切面也没有地方写入 params.dataScope。
对应 XML 往往是这样的:
<delete id="deleteUserById" parameterType="Long">
update sys_user
set del_flag = '2'
where user_id = #{userId}
</delete>
这条 SQL 本身没有数据权限条件。
当然,若依并不是完全不处理这类问题。它通常会在业务层增加额外校验,例如:
public void checkUserDataScope(Long userId) {
if (!SecurityUtils.isAdmin()) {
SysUser user = new SysUser();
user.setUserId(userId);
List<SysUser> users =
SpringUtils.getAopProxy(this).selectUserList(user);
if (StringUtils.isEmpty(users)) {
throw new ServiceException("没有权限访问用户数据");
}
}
}
这个方法的思路是:
- 先构造一个
SysUser; - 设置需要校验的
userId; - 再调用带
@DataScope的列表查询; - 如果查询不到,说明当前用户无权访问该数据。
这个兜底方案是可用的,但它属于业务层主动补救。
也就是说,若依在按 ID 删除、按 ID 修改、批量操作等场景中,依赖开发者记得手动调用:
checkUserDataScope(userId);
checkRoleDataScope(roleId);
checkDeptDataScope(deptId);
如果新增模块、新接口或新删除逻辑忘了调用,数据权限就可能被绕过。
因此,若依方案的核心问题并不是“不能处理 ID 删除”,而是处理方式比较分散:
- 注解负责触发;
BaseEntity.params负责传递 SQL;- XML 负责拼接 SQL;
- ID 场景依赖额外
checkXXXDataScope; - 最终是否完整,依赖开发者是否记得所有约定。
对于大型项目来说,这种模式的遗漏风险会比较高。
四、Geek-XD 的实现思路
Geek-XD 保留了 @DataScope 注解的使用习惯,但真正关键的改进在于:
数据权限不再只依赖
BaseEntity.params和 XML 手工拼接,而是通过 MyBatis-Flex 方言,在 SQL 生成阶段统一注入。
Geek-XD 中仍然可以这样使用数据权限注解:
@Override
@DataScope(deptAlias = "d", userAlias = "u")
public Page<SysUser> page(SysUser user, int pageNum, int pageSize) {
Page<SysUser> pg = Page.of(pageNum, pageSize);
pg.setOptimizeCountQuery(false);
return selectUserList(user).page(pg);
}
查询则大量使用 MyBatis-Flex 的链式 API:
public QueryChain<SysUser> selectUserList(SysUser user) {
QueryChain<SysUser> queryChain = this.queryChain()
.select(SYS_USER.DEFAULT_COLUMNS, SYS_DEPT.DEPT_NAME, SYS_DEPT.LEADER)
.from(SysUser.class).as("u")
.leftJoin(SysDept.class).as("d")
.on(SysUser::getDeptId, SysDept::getDeptId)
.eq(SysUser::getUserId, user.getUserId())
.like(SysUser::getUserName, user.getUserName())
.eq(SysUser::getStatus, user.getStatus())
.like(SysUser::getPhonenumber, user.getPhonenumber())
.ge(SysUser::getCreateTime, user.getParams().get("beginTime"))
.le(SysUser::getCreateTime, user.getParams().get("endTime"));
return queryChain;
}
如果仍然依赖 XML 中的 ${params.dataScope},这种链式查询模式就很难优雅地统一处理数据权限。
Geek-XD 的做法是:切面仍然负责计算当前用户的数据范围 SQL,但不再只把它塞进实体参数中,而是放入当前线程上下文。
核心代码如下:
if (StringUtils.isNotBlank(sqlString.toString())) {
if (joinPoint.getArgs().length != 0) {
Object params = joinPoint.getArgs()[0];
if (StringUtils.isNotNull(params) && params instanceof BaseEntity) {
BaseEntity baseEntity = (BaseEntity) params;
baseEntity.getParams().put(
DATA_SCOPE,
" AND (" + sqlString.substring(4) + ")"
);
}
}
DataScopeContextHolder.use("(" + sqlString.substring(4) + ")");
}
这里最关键的是:
DataScopeContextHolder.use(...)
DataScopeContextHolder 本质上是一个 ThreadLocal 栈:
public class DataScopeContextHolder {
private static ThreadLocal<Deque<String>> lookup =
ThreadLocal.withInitial(ArrayDeque::new);
private DataScopeContextHolder() {
}
public static void use(String dataScope) {
lookup.get().push(dataScope);
}
public static String get() {
Deque<String> deque = lookup.get();
return deque.peek();
}
public static void clear() {
Deque<String> deque = lookup.get();
if (!deque.isEmpty()) {
deque.pop();
if (deque.isEmpty()) {
lookup.remove();
}
}
}
public static void forceClear() {
lookup.remove();
}
}
方法执行结束后,切面会清理上下文:
@After(value = "@annotation(controllerDataScope)")
public void doAfter(final JoinPoint point, DataScope controllerDataScope) {
DataScopeContextHolder.clear();
}
这样,数据权限条件就不再必须挂在某个实体参数上,而是跟随当前业务调用线程存在。
五、核心改造:MyBatis-Flex 方言注入
Geek-XD 最核心的设计在 DataScopeDialectImpl。
它扩展了 MyBatis-Flex 的方言能力,在 SQL 生成阶段读取 DataScopeContextHolder 中的数据权限 SQL,然后统一追加到 QueryWrapper 或 SQL 片段中。
针对 QueryWrapper 的处理如下:
private static void prepareAuth(
QueryWrapper queryWrapper,
OperateType operateType
) {
String dataScopeSql = DataScopeContextHolder.get();
if (StringUtils.isEmpty(dataScopeSql)) {
return;
}
List<QueryTable> queryTables = CPI.getQueryTables(queryWrapper);
if (queryTables == null || queryTables.isEmpty()) {
return;
}
QueryTable t = queryTables.get(0);
if (queryTables.size() == 1
&& "t".equals(t.getAlias())
&& t.getName() == null
&& t.getSchema() == null) {
QueryWrapper child = CPI.getChildSelect(queryWrapper).get(0);
child.and(dataScopeSql);
} else {
queryWrapper.and(dataScopeSql);
}
}
针对普通 SQL 构建的处理如下:
private static void prepareAuth(
String schema,
String tableName,
StringBuilder sql,
OperateType operateType
) {
String dataScopeSql = DataScopeContextHolder.get();
if (StringUtils.isNotEmpty(dataScopeSql)) {
sql.append(AND).append(dataScopeSql);
}
}
针对表信息构建的处理如下:
private static void prepareAuth(
TableInfo tableInfo,
StringBuilder sql,
OperateType operateType
) {
String dataScopeSql = DataScopeContextHolder.get();
if (StringUtils.isNotEmpty(dataScopeSql)) {
sql.append(AND).append(dataScopeSql);
}
}
也就是说,只要当前线程中存在数据权限上下文,MyBatis-Flex 在生成 SQL 时就可以自动把数据权限条件加进去。
Geek-XD 还在 MyBatis-Flex 配置中为所有数据库类型注册自定义方言:
@Bean
public MyBatisFlexCustomizer myBatisFlexCustomizer(KeyConfig keyConfig) {
return new MyBatisFlexCustomizer() {
@Override
public void customize(FlexGlobalConfig flexGlobalConfig) {
DbType.all().forEach(dbtype -> {
DialectFactory.registerDialect(
dbtype,
DataScopeDialectImpl.createDialect(dbtype)
);
});
BaseEntityListener baseEntityListener = new BaseEntityListener();
flexGlobalConfig.registerInsertListener(baseEntityListener, BaseEntity.class);
flexGlobalConfig.registerUpdateListener(baseEntityListener, BaseEntity.class);
flexGlobalConfig.setLogicDeleteColumn("del_flag");
flexGlobalConfig.setTenantColumn("tenant_id");
flexGlobalConfig.setKeyConfig(keyConfig);
}
};
}
这一步非常关键。
若依的数据权限是在“业务参数 + XML 占位”层面完成的。
Geek-XD 的数据权限则进入了“ORM SQL 生成”层面。
六、为什么 Geek-XD 对按 ID 操作更友好
按 ID 查询、删除、批量删除这类操作,参数通常是:
Long id
List<Long> ids
Long[] ids
这些参数都不是 BaseEntity。
在若依模式下,@DataScope 没有办法自然地把权限 SQL 放进这些参数里,所以必须额外写:
checkUserDataScope(userId);
checkRoleDataScope(roleId);
checkDeptDataScope(deptId);
这依赖业务开发者每次都记得调用。
而 Geek-XD 的优势是:权限 SQL 不再依赖方法参数承载,而是放入当前线程上下文,并在 MyBatis-Flex 生成 SQL 时统一注入。
例如 Geek-XD 中可以这样写:
@Override
@DataScope(deptAlias = "d", userAlias = "u")
public void checkUserDataScope(Long userId) {
if (!SecurityUtils.isAdmin()) {
SysUser user = new SysUser();
user.setUserId(userId);
List<SysUser> users = selectUserList(user).list();
if (StringUtils.isEmpty(users)) {
throw new ServiceException("没有权限访问用户数据");
}
}
}
注意,这个方法的参数是 Long userId。
如果是传统若依模式,Long 本身无法承载 params.dataScope。
但在 Geek-XD 中,@DataScope 建立的是 ThreadLocal 权限上下文。后续 selectUserList(user).list() 走 MyBatis-Flex QueryWrapper 时,方言层会自动追加数据权限条件。
这就把数据权限从:
参数能不能带上 SQL
提升到了:
当前 ORM SQL 是否处于权限上下文中
这也是 Geek-XD 方案在架构上更可靠的地方。
七、两种方案对比
| 对比项 | 若依方案 | Geek-XD 方案 |
|---|---|---|
| 权限触发方式 | @DataScope 注解 |
@DataScope 注解 |
| 权限 SQL 载体 | BaseEntity.params.dataScope |
DataScopeContextHolder 线程上下文 |
| SQL 生效方式 | XML 中手写 ${params.dataScope} |
MyBatis-Flex 方言 prepareAuth 统一注入 |
| 对方法参数要求 | 第一个参数最好是 BaseEntity 子类 |
对参数形态更宽容 |
| 按 ID 删除 | 通常依赖额外 checkXXXDataScope |
可通过上下文 + ORM SQL 生成链路统一处理 |
| 链式查询支持 | 不天然适配 | 适配 QueryWrapper、QueryChain |
| 遗漏风险 | 注解、参数、XML、手工 check 都要记得 | 主要收敛到注解和 ORM 受控链路 |
| 框架闭环程度 | 偏业务层约定 | 偏 ORM 层统一治理 |
从表中可以看出,两者不是简单的“有没有数据权限”的区别,而是数据权限放在哪一层治理的问题。
若依更偏向业务层和 XML 层配合。
Geek-XD 更偏向 ORM 层统一注入。
八、Geek-XD 方案的边界
客观来说,Geek-XD 的方案也不是“所有 SQL 自动安全”。
它仍然有几个前提:
- 业务方法需要正确标注
@DataScope; - SQL 执行路径最好走 MyBatis-Flex 的 QueryWrapper、QueryChain、Service API 等受控链路;
- 如果绕过 MyBatis-Flex,直接使用原生 JDBC 或完全手写 SQL,仍然需要单独治理;
- 复杂 SQL 的表别名需要和
@DataScope(deptAlias = "d", userAlias = "u")保持一致。
所以,Geek-XD 的优势不是“魔法式解决所有权限问题”,而是它把数据权限的默认生效位置下沉到了 ORM 层,减少了业务代码手工拼接和手工检查的比例。
这对后台管理框架非常有价值。
九、总结
若依的数据权限实现简单、清晰,适合传统 MyBatis XML 项目。
它通过 @DataScope 生成 SQL 片段,再通过 ${params.dataScope} 拼接到 XML 中。这个方案在列表查询场景中很直观,但它的限制也很明显:
- 数据权限依赖
BaseEntity.params; - SQL 需要手动写
${params.dataScope}; - 按 ID 删除、按 ID 修改、批量操作等场景需要额外业务校验;
- 最终是否完整,依赖开发者是否遵守所有约定。
Geek-XD 在保留 @DataScope 使用习惯的基础上,结合 MyBatis-Flex 的方言扩展,把数据权限条件放入 ThreadLocal 上下文,并在 QueryWrapper/SQL 生成阶段统一注入。
这使得 Geek-XD 的数据权限方案具备更强的框架闭环能力:
- 不再强依赖方法第一个参数是
BaseEntity; - 不再强依赖 XML
${params.dataScope}; - 更适合 QueryWrapper/QueryChain 链式查询;
- 对按 ID 操作、批量操作等场景更友好;
- 更能降低业务开发者遗漏权限校验的风险。
因此,从源码实现和工程可靠性角度看,Geek-XD 在数据权限这一块的设计,比传统若依模式更进一步。
它不是简单地“换了一个 ORM”,而是借助 MyBatis-Flex 的方言扩展,把数据权限从业务层约定推进到了 ORM 层统一治理。
对于后台管理系统中最容易出问题的“列表看不到,但可能通过 ID 操作越权”的场景,Geek-XD 的方案明显更接近完整闭环。
更多推荐




所有评论(0)