第九章. common-core 工具类(七):SqlUtil 与 PageQuery 分页封装
第九章. common-core 工具类(七):SqlUtil 与 PageQuery 分页封装
Prorise第九章. common-core 工具类(七):SqlUtil 与 PageQuery 分页封装
摘要:本章我们将深入 SqlUtil。在 MyBatis-Plus (MP) 已经解决大部分 SQL 注入的背景下,我们将通过分析 RVP 框架的 分页实体 PageQuery,来理解 SqlUtil 存在的真正价值。它并不是 MP 的替代品,而是 RVP 在处理 前端动态排序字符串 时的“最后一道防线”。
在上一章中,我们深入剖析了 ReflectUtils,它继承了 Hutool ReflectUtil 并提供了核心增强。我们掌握了 Hutool 反射的基础用法(newInstance, setFieldValue, invoke),并重点实战了 RVP 独有的 invokeGetter 和 invokeSetter 在多级嵌套属性访问中的应用。
现在,我们来看 utils 包下一个非常“特殊”的工具类——SqlUtil。
在 RVP 这种深度整合了 MyBatis-Plus (MP) 的项目中,我们几乎所有的 SQL 操作(尤其是 ORDER BY 和 WHERE)都是通过 MP 的 Wrapper 来动态、安全地构建的。
这就带来了一个尖锐的问题:既然 MP 已经帮我们防止了 SQL 注入,RVP 为什么还要单独提供一个 SqlUtil?它在“二开”中的真实用途到底是什么?
本章,我们将从这个“灵魂拷问”出发,深入 SqlUtil 的设计意图和 RVP 框架中 唯一 使用它的真实场景——PageQuery 实体。
本章学习路径
9.1. SqlUtil 源码解析:“白名单”与“黑名单”
我们首先必须理解 SqlUtil 这个工具本身。
9.1.1. 文件定位与源码概览
我们打开 SqlUtil 的源码:
文件路径:ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/sql/SqlUtil.java
这个类非常简洁,其核心就是两个 String 常量(正则表达式)和三个 static 方法。
1 | package org.dromara.common.core.utils.sql; |
SqlUtil 提供了两种截然不同的安全校验策略:“白名单”和“黑名单”。
9.1.2. “白名单”校验:SQL_PATTERN 与 isValidOrderBySql
SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+";- 这是一个 白名单 正则表达式。它定义了“允许”的字符集:
a-zA-Z0-9_:字母、数字、下划线(合法的字段名)\\:空格(例如ORDER BY id asc)\\,:逗号(例如id,create_time)\\.:点(例如user.id)
- 这是一个 白名单 正则表达式。它定义了“允许”的字符集:
isValidOrderBySql(String value)- 此方法用于判断
value字符串是否 完全由SQL_PATTERN中的“安全字符”组成。 - 只要
value中出现 任何 白名单之外的字符(例如*、;、(、)、-),此方法都会返回false。
- 此方法用于判断
9.1.3. “黑名单”校验:SQL_REGEX 与 filterKeyword
SQL_REGEX = "...|and |extractvalue|updatexml|sleep|..."- 这是一个 黑名单 字符串。它列出了所有“禁止”出现的 SQL 注入高危关键字。
- 注意:关键字(如
select)后面特意带了一个空格,这是为了防止误杀(例如字段名username包含user)。
filterKeyword(String value)- 此方法会遍历
SQL_REGEX列表,检查value中是否 包含 任意一个“高危关键字”(忽略大小写)。 - 只要
value包含(indexOfIgnoreCase > -1)了SQL_REGEX中的任意一个(例如" and "),此方法就会 立即抛出异常。
- 此方法会遍历
9.1.4. 核心方法:escapeOrderBySql (白名单的应用)
escapeOrderBySql 是 isValidOrderBySql 的“应用层”封装。它 只使用“白名单”(SQL_PATTERN)。
它的逻辑是:
- 检查
value是否非空。 - 如果非空,就调用
isValidOrderBySql(value)进行白名单校验。 - 如果校验 不通过(
!isValidOrderBySql为true),说明value包含了非法字符(如;),立即抛出IllegalArgumentException,终止程序。 - 如果校验通过,原样返回
value。
9.2. SqlUtil 的存在价值:为何 MyBatis-Plus (MP) 不够用?
现在我们理解了 SqlUtil 是一个“校验器”。但正如我们之前讨论的,MyBatis-Plus (MP) 本身就防 SQL 注入,为什么还需要它?
9.2.1. MP 的安全机制:Wrapper 与预编译
MyBatis-Plus (MP) 框架的核心价值之一就是 防止 SQL 注入。它通过 Wrapper(条件构造器)和 预编译(PreparedStatement)机制,确保了 WHERE 条件的安全。
1 | // MP 的“安全”用法 (WHERE) |
对于排序,MP 同样提供了 类型安全 的方法:
1 | // MP 的“安全”用法 (ORDER BY) |
这种写法通过 Lambda 表达式引用 User::getCreateTime,在编译期就固定了 create_time 字段,无法 通过前端传参来动态修改这个排序字段。
9.2.2. 框架的痛点:前端动态排序与原生 SQL 拼接风险
MP 的安全用法 orderByAsc(User::getCreateTime) 虽然安全,但它“不够灵活”。在 RVP 这种高度工程化的后台中,前端表格需要支持 点击任意列头(如“用户 ID”、“登录时间”、“状态”)进行动态排序。
前端发送的请求(封装到 PageQuery DTO 中)通常是这样的:{ "orderByColumn": "id,createTime", "isAsc": "asc,desc" }
后端 Controller 拿到这个 orderByColumn 字符串("id,createTime")后,无法直接用于类型安全的 Lambda。如果直接将这个字符串拼接到 SQL 中,MP 的安全机制就失效了。
这就是 MP 机制之外的“风险点”:
1 | // 危险的“拼接” SQL (Bad Practice) |
9.2.3. SqlUtil 的定位:原生 ORDER BY 字符串的“校验器”
SqlUtil 正是为了堵住 9.2.2 中的“风险点”而诞生的。
它 不是 MyBatis-Plus 的替代品,它只做一件事:在 RVP 框架必须拼接原生 ORDER BY 字符串时,充当“最后一道防线”,确保这个字符串是“干净的”。
它通过 escapeOrderBySql(白名单)确保前端传入的 orderByColumn 字符串中,绝对不包含 任何 ;, *, ( 等危险字符,从而在根源上杜绝了 ORDER BY 注入。
9.3. 真实场景追踪:PageQuery 如何应用 SqlUtil
现在,我们定位到 RVP 框架中 唯一 使用 SqlUtil 的地方:PageQuery 实体类。
9.3.1. 实体定位:PageQuery (分页 DTO)
PageQuery 是 RVP 中所有分页查询 Controller 的标准入参。它是一个 DTO (Data Transfer Object),专门用于 承接 前端(如 plus-ui)发送过来的分页和排序参数。
文件路径:ruoyi-common/ruoyi-common-mybatis/src/main/java/org/dromara/common/mybatis/core/page/PageQuery.java
PageQuery 的核心字段:
1 |
|
9.3.2. 源码追踪:build() -> buildOrderItem()
PageQuery 的核心方法是 build(),它负责将这个 DTO 转换为 MP 认识的 Page<T> 对象。
1 | // 位于 PageQuery.java |
build() 方法将排序的“脏活累活”委托给了私有方法 buildOrderItem()。
9.3.3. 【SqlUtil 唯一登场点】escapeOrderBySql 的作用
buildOrderItem() 是本章的核心,SqlUtil 在这里扮演了“守门员”的角色。
我们来精读 buildOrderItem() 的 关键首行:
1 | // 位于 PageQuery.java |
分析:SqlUtil.escapeOrderBySql(orderByColumn) 是整个安全体系的 第一道关卡。
- 黑客传入:
orderByColumn = "id; DROP TABLE users"。 escapeOrderBySql调用isValidOrderBySql。isValidOrderBySql使用SQL_PATTERN(白名单)进行matches校验。"id; DROP TABLE users"中包含了 非法字符;。isValidOrderBySql返回false。escapeOrderBySql抛出IllegalArgumentException("参数不符合规范...")。build()方法执行失败,Controller层捕获异常并返回错误信息给前端。- SQL 注入防御成功。
9.3.4. 源码概览:PageQuery 实体对排序逻辑的完整封装
在通过 SqlUtil 的安全校验后,buildOrderItem 继续执行后续的业务逻辑:
1 | private List<OrderItem> buildOrderItem() { |
总结:PageQuery 这个 DTO 通过 buildOrderItem 方法,完美地实现了 从“前端不安全的动态排序列字符串”到“MP 安全的 List<OrderItem> 对象”的转换,而 SqlUtil.escapeOrderBySql 正是这个转换过程中不可或缺的第一道“安全门”。
9.4. 测试准备:创建 SqlUtilTest (main 方法)
SqlUtil 是一个纯 Java 工具类,不依赖 Spring 容器。我们可以使用 main 方法来测试它。
9.4.1. 创建 utils.test.SqlUtilTest.java
我们在 ruoyi-demo 模块中创建测试类:
文件路径:ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/utils/test/SqlUtilTest.java
1 | package org.dromara.demo.utils.test; |
9.4.2. 编写 main 方法
我们将在 main 方法中调用后续的测试。
9.5. 防线(一):isValidOrderBySql 与 escapeOrderBySql
这组方法是基于**“白名单”**(SQL_PATTERN)的校验,这也是 PageQuery 正在使用的防线。
9.5.1. 源码解析:SQL_PATTERN 正则表达式
我们再看一眼“白名单”规则:public static final String SQL_PATTERN = "[a-zA-Z0-9_\\ \\,\\.]+";
它 只允许 字母、数字、下划线、空格、逗号、点 这几种字符存在。
9.5.2. 实战 isValidOrderBySql:合法的 vs 非法的
isValidOrderBySql 方法用于检查一个字符串是否 完全 由上述安全字符组成。
我们在 SqlUtilTest 中创建 testWhiteList 方法:
1 | // ... |
运行 main 方法,控制台输出:
1 | ... INFO ... SqlUtil 测试开始... |
分析:白名单的校验非常严格。任何可能导致 SQL 语法改变的特殊字符(如 ;, *, (, ))都会被直接拦截,返回 false。
9.5.3. 实战 escapeOrderBySql:安全通过 vs 抛出异常
escapeOrderBySql 是 PageQuery 中 真正使用 的方法。它调用 isValidOrderBySql,如果返回 false,它会 抛出异常 来中断程序。
1 | // ... 在 testWhiteList() 方法中继续添加 ... |
运行 main 方法(接上文输出):
1 | ... INFO ... --- 2. 测试 escapeOrderBySql (白名单应用) --- |
结论:escapeOrderBySql 成功扮演了“断路器”的角色,它在检测到非法字符 ; 时,立即抛出异常,阻止了恶意 SQL 的下一步拼接。
9.6. 防线(二):filterKeyword(“黑名单”校验)
SqlUtil 提供了第二种防御机制:filterKeyword,它基于“黑名单”(SQL_REGEX)。
9.6.1. 源码解析:SQL_REGEX 关键字列表
public static String SQL_REGEX = "...|and |extractvalue|updatexml|sleep|exec |insert |select |delete |..."
filterKeyword 方法会检查输入值是否 包含(indexOfIgnoreCase)这个列表中的 任意一个 关键字。
9.6.2. 【二开场景】何时使用?(为何 PageQuery 不用它?)
在“二开”中,我们必须思考:PageQuery 为什么 不用 filterKeyword,而是用了 escapeOrderBySql?
escapeOrderBySql(白名单)更严格:它只认[a-z0-9_ ,.]。*、()都不认识,直接拒绝。filterKeyword(黑名单)更宽松:它只检查select,drop等关键字。如果你传入*或name(),filterKeyword是 允许通过 的,因为*和name()不在它的“黑名单”上。ORDER BY的特殊性:ORDER BY后面只应该出现 字段名。字段名是受“白名单”严格约束的。WHERE的特殊性:WHERE后面的值(例如name = '张三')可能包含各种字符,此时用“白名单”会误杀。
结论:
escapeOrderBySql(白名单):专用于ORDER BY排序字段名。PageQuery的选择是正确的。filterKeyword(黑名单):专用于WHERE条件中的值,当你无法使用 MP 的?预编译,且必须拼接WHERE值时,作为最后的防御手段。(二开时极不推荐,应始终使用Wrapper.eq())。
9.6.3. 实战 filterKeyword:select vs select (带空格) 的陷阱
filterKeyword 有一个非常容易误解的“陷阱”。
我们在 SqlUtilTest 中创建 testBlackList 方法:
1 | // ... |
运行 main 方法,控制台输出:
1 | ... INFO ... --- 3. 测试 filterKeyword (黑名单) --- |
分析:filterKeyword 的黑名单 SQL_REGEX 中包含的是 "select "(带空格)。
indexOfIgnoreCase("select", "select ")返回-1(不匹配),因此SqlUtil.filterKeyword("select")会被放行!indexOfIgnoreCase("select ", "select ")返回0(匹配),因此SqlUtil.filterKeyword("select ")会被拦截。- 这是 RVP 为了防止误杀(例如
username包含user)而故意设计的,我们在使用时必须清楚这个“带空格”的约定。









