order by多字段不能参数化是sql语法硬限制,因数据库仅允许参数绑定“值”而非“标识符”,预编译不支持列名、排序方向等语法成分;多字段需逐个白名单校验,漏一即注入。

ORDER BY 多字段为什么不能直接参数化
数据库引擎根本不允许把表名、列名、排序方向这些语法成分当参数传。你写 ORDER BY ? 或 ORDER BY @col,SQL Server 会报 Incorrect syntax near '@',MySQL 报 You have an error in your SQL syntax。这不是 MyBatis 或 JDBC 的锅,是 SQL 标准和执行器的硬限制。多字段排序(比如 ORDER BY status, created_at DESC)只是让问题更明显——每个字段名、每个 ASC/DESC 都得单独校验,漏一个就等于开后门。
MyBatis-Plus 动态多字段排序的白名单落地方式
别信 QueryWrapper.orderByAsc("user_name") 能防注入,它底层仍是字符串拼接;也别用 ${sortFields} 直接接前端字符串。正确路径是:前端传类似 sort=name,created_at&order=asc,desc,后端拆成字段数组 + 方向数组,再逐个映射+校验。
- 定义白名单枚举:
SortColumnEnum.NAME("user_name")、CREATED_AT("created_at"),不支持的字段名直接拒掉 - 方向只接受
"asc"和"desc"(大小写统一转小写再比),其他值一律 fallback 到"asc" - 字段数和方向数必须一致,不等就截断或报错,避免
name,age对应asc导致第二个字段无方向 - 最终用
LambdaQueryWrapper构建时,循环调用orderByAsc(Users::getName)、orderByDesc(Users::getCreatedAt)—— 方法引用在编译期锁定列名,彻底避开字符串拼接
XML 中多字段 ${} 拼接的唯一安全边界
如果非要用 XML 写动态 SQL(比如要兼容旧逻辑或复杂表达式),${} 不是不能用,而是必须确保它只接收白名单校验后的结果。不要在校验前拼,更不要在校验后还做二次字符串处理。
- 校验必须在进入 SQL 拼接前完成:先解析
sort=name,updated_at→ 映射为["user_name", "updated_at"]→ 检查每个是否在白名单中 → 全部通过才组装 - 拼接时字段名加反引号(MySQL)或双引号(PostgreSQL):
ORDER BY `#{col0}` ${dir0}, `#{col1}` ${dir1},防止字段名含空格或关键字 - 禁止任何“兜底”操作:比如
REPLACE(${rawSort}, ';', '')或正则删注释符,对name; DROP TABLE--这类变体完全无效 - 字段数建议设上限(如最多 3 个),超限直接 400,避免生成过长 SQL 或拖慢解析
PostgreSQL/SQL Server 存储过程中多字段动态排序的 CASE 替代法
存储过程里拼字符串最危险,也最容易被忽略。SQL Server 用 CASE 做分支,PostgreSQL 可用 NULLS FIRST/LAST 配合,核心都是把字段名“写死”,只让参数控制走向。
- SQL Server 示例:
ORDER BY CASE @sortField WHEN 'name' THEN name WHEN 'status' THEN status END * CASE @sortDir WHEN 1 THEN -1 ELSE 1 END—— 注意类型要一致,否则报转换错 - PostgreSQL 更简洁:
ORDER BY (CASE $1 WHEN 'name' THEN name WHEN 'price' THEN price END) $2 NULLS LAST,其中$2是'ASC'或'DESC'参数 - 字段名必须全部出现在
CASE分支中,不能靠默认ELSE放行未知字段 - 多字段时,每个字段独立写一套
CASE,不要试图共用一个分支变量
白名单不是加个配置项就完事,它得覆盖所有业务场景下的合法输入,包括别名、带下划线的物理列名、甚至 JSON 字段路径(如 data->>'score')。一旦漏掉一个真实使用的字段,开发就会绕过校验,或者临时打补丁拼字符串——那道防线就从内部瓦解了。











