@query + @param 仅防参数值注入,无法防御动态表名、字段名、排序字段等sql元数据注入,因其绕过preparedstatement;必须通过白名单校验或criteria api等机制保障安全。

为什么 @Query + @Param 不能防住所有注入
@Query 配合 @Param 确实能防住参数值注入,但前提是整个 SQL 字符串是静态的。一旦出现 ${}、#{} 或运行时拼接表名/字段名,防线就立刻失效——因为这些语法在 JPA 解析阶段就被展开为原始字符串,绕过了 PreparedStatement 的预编译机制。
常见错误场景包括:@Query("SELECT * FROM user WHERE name LIKE '%${name}%'")、@Query(value = "SELECT * FROM #{#entityName}", nativeQuery = true)、@Query("SELECT * FROM user ORDER BY #{#sortField}")。这些写法让恶意输入直接进入 SQL 结构层,数据库解析器会把它当指令执行,不是值。
-
${}是 SpEL 字符串插值,等同于 Java 的+拼接 -
#{}在 nativeQuery 中用于动态元数据(如表名),无法参数化 - ORDER BY、GROUP BY、LIMIT 后的字段或数字,都不能用
:param占位
动态表名/字段名必须走白名单校验
表名、列名、排序字段这些属于 SQL 元数据,JDBC 和 JPA 天然不支持参数化。你不能写 SELECT :column FROM :table,运行时会报错。唯一安全的做法是:把允许的值固化为枚举或配置项,从用户输入映射到预设值,而不是直接代入。
例如排序字段只允许 "id"、"created_time"、"status",那就用 switch 或 Map.of() 映射,遇到非法值直接抛 IllegalArgumentException。
- 禁止把
request.getParameter("sort")直接传进@Query字符串 - 白名单必须硬编码或加载自受控配置(如
@ConfigurationProperties),不能来自数据库或外部 API - SonarQube 报告的“SQL 注入”警告,在白名单场景下是误报,应配合
@SuppressWarnings("squid:CallToDeprecatedMethod")或注释说明校验逻辑
复杂动态查询优先用 Criteria API 或 Specification
当条件组合多变(比如搜索页有 5 个可选字段)、且需保持类型安全时,手写 @Query 很快会失控。Criteria API 和 Spring Data JPA 的 Specification 不拼字符串,靠 Java 对象反射生成 SQL,所有字段名、操作符都来自实体类定义,天然免疫注入。
示例中 root.get("username") 的 "username" 是编译期确定的字符串字面量,不是运行时变量;criteriaBuilder.like(...) 的值始终走 PreparedStatement 绑定。
- 简单单条件查询,
@Query+@Param足够,别过度设计 - 涉及 or/and 嵌套、权限过滤、多租户字段追加时,
Specification的组合能力(.and()/.or())比 if-else 拼 SQL 更可靠 - 注意
Specification不能替代白名单——它只管值和字段名,不管表名或排序方向(ASC/DESC)
LIKE 查询和模糊匹配的写法陷阱
LIKE 的通配符(%)必须由代码拼,不能交给用户输入。错误写法:@Query("SELECT u FROM User u WHERE u.name LIKE :pattern") + @Param("pattern") "%"+input+"%" —— 这看似安全,但若 input 包含 _ 或 %,就会改变语义。更糟的是,有人写成 "%${input}%",直接破防。
正确做法是:在 Java 层处理通配逻辑,再传纯参数值。JPA 支持 criteriaBuilder.like(root.get("name"), "%" + name + "%"),也支持原生查询中用 CONCAT('%', :name, '%'),但绝不能让前端控制 % 位置。
- 用户输入的
name应先做长度限制(如 ≤ 50 字符),防止慢查询 - 若需支持前缀/后缀/全匹配,用独立参数区分,如
matchMode = "prefix",再由服务端决定加几个% - 避免在 SQL 里用
ESCAPE处理特殊字符——增加复杂度且易出错,不如在 Java 层 clean 输入
真正难的不是写对一行 @Query,而是识别哪些地方“看起来像参数,其实不是参数”。表名、字段名、排序方向、分组维度、函数名……这些都得在代码里划清边界,用白名单卡死,而不是依赖框架的“自动防护”。










