真正防注入的是preparedstatement机制,而非?1语法本身;?1仅保护值位置,表名、列名、order by等结构拼接仍可导致注入,必须白名单校验或硬编码。

原生SQL里用 ?1、?2 绑定参数本身不防注入,真正防注入的是 PreparedStatement 机制——但前提是别在 SQL 字符串里拼用户输入。
为什么 ?1 绑定看起来安全却仍可能被绕过
很多人误以为写了 ?1 就万事大吉,其实不然。JDBC 的预编译只保护“值”的位置,不保护“结构”。只要你在 SQL 字符串中动态拼接了表名、列名、ORDER BY 方向、函数名等,?1 就完全失效。
- 错误写法:
@Query(value = "SELECT * FROM " + tableName + " WHERE id = ?1", nativeQuery = true)——tableName是用户可控的,直接拼进字符串,?1挡不住 - 错误写法:
@Query(value = "SELECT * FROM user ORDER BY ?1", nativeQuery = true)——?1在 ORDER BY 后当标识符用,JDBC 不会转义,数据库直接解析为字段名 - 正确前提:所有非值内容(表名、列名、排序方向、GROUP BY 字段等)必须硬编码,或经白名单校验后拼接
手动索引绑定(?1 / ?2)的使用限制与陷阱
位置参数在原生 SQL 中可用,但有明确边界:仅适用于 WHERE、HAVING、SET、VALUES 等值上下文,且顺序必须严格对应方法参数顺序。
- 参数顺序错一位,整个查询就绑定错位,比如
?1绑到密码字段,?2绑到用户名,登录逻辑直接翻车 - 不能跨查询复用索引:主查询用了
?1,countQuery 也得用?1(即使它根本不需要这个参数),否则抛Could not locate ordinal parameter [1] - IN 列表无法用
?1直接展开:写WHERE id IN ?1会报错;必须拆成WHERE id IN (?1, ?2, ?3)并传入固定长度参数,或改用JdbcTemplate动态构造 - 数据库函数内慎用:如
CONCAT(?1, ?2)或UPPER(?1)虽语法合法,但某些驱动/方言可能绕过参数化,建议改用 Java 层拼接后再传入
什么时候必须放弃 ?1,改用命名参数 :param
当方法参数多于 2 个、或存在可选参数(如 null 默认值)、或需要复用同一参数多次时,?1 极易出错,:param 是更健壮的选择。
-
@Param注解不可省:没有@Param("email"),框架无法把方法参数映射到:email,运行时报Parameter with name 'email' not found - 命名参数不支持在 ORDER BY / GROUP BY 中动态指定方向:写
ORDER BY name :direction无效;必须 Java 层判断后硬编码"ORDER BY name ASC"或"ORDER BY name DESC" - 命名参数在 countQuery 中同样要出现:哪怕 countQuery 逻辑上不需要,也得加个无害占位符,例如
countQuery = "SELECT COUNT(*) FROM user WHERE 1 = 1 AND ?1 IS NOT NULL",让参数绑定能通过
真正防注入的关键动作不是怎么写 ?1,而是守住拼接边界
团队里最常被忽略的漏洞点,是 service 层用 String.format 或 StringBuilder 拼完 SQL 再丢给 JdbcTemplate —— 这种代码既不报编译错,也不触发 JPA 的任何检查,但等于裸奔。
- 所有动态表名/列名必须走白名单:比如只允许
Set.of("user", "order", "product"),不在其中就throw new IllegalArgumentException() - 所有用户输入进 SQL 值位置前,强类型校验优先:手机号用
String.matches("\d{11}"),状态码用enum,ID 用Long.parseLong()再 catch NumberFormatException - LIKE 模糊匹配必须写死通配符:
name LIKE %:name%中的%不能来自参数,否则:name传入"a%b_c"就可能触发 ESCAPE 或下划线匹配意外行为 - 最保险的路径永远是:不用原生 SQL 就不用担惊受怕 ——
findByEmailAndStatusIn()这类派生方法,连@Query都不写,天然 PreparedStatement,零配置防注入











