Specification本身不防注入,真正防注入的是规范使用CriteriaBuilder构建谓词且避免字符串拼接和反射字段名;字段名应白名单校验,用户输入值通过setParameter绑定,通配符须写死,IN查询等动态操作应利用Specification内置方法。

Specification 本身不防注入——它只是个接口;真正防注入的是你怎么用 CriteriaBuilder 构建谓词,以及是否跳过字符串拼接和反射字段名。只要写法合规,它比手写 @Query 更安全。
为什么 cb.like(root.get("name"), "%" + name + "%") 安全?
这个调用里只有两处用户输入:字段名 "name" 和值 name。前者是硬编码字符串,后者全程走 setParameter() 绑定,不会进 SQL 字符串生成流程。
-
"name"是 Java 字面量,编译期确定,无法被篡改 -
name变量只参与cb.like(...)的参数构造,Hibernate 最终把它塞进 PreparedStatement 的?占位符 - 哪怕
name = "admin' --",结果也只是查邮箱以该字符串开头的记录,不会闭合引号或注释后续 SQL -
"%"是代码里写死的,不是从请求里来的——这点极易被忽略
root.get(orderBy) 为什么会抛 IllegalArgumentException?
因为 root.get(String) 要求字段名必须对应实体类的属性,且在编译期/运行时能被元数据识别。它不支持任意字符串反射取字段,这反而是安全设计。
- 前端传
sort=name;order=desc,你不能直接root.get(sort) - 必须白名单校验:
if (!Set.of("username", "createdAt", "score").contains(sort)) throw new IllegalArgumentException(); - 或者用枚举+switch:
switch (sort) { case "username" -> root.get("username"); } - 绕过这个限制(比如用反射强行 get)等于主动放弃 JPA 的类型安全层
IN 查询、空条件、模糊匹配怎么写才不出错?
这些看似“动态”的操作,Specification 都有内建支持,无需手动拼字符串或补 1=1。
-
root.get("id").in(ids):Hibernate 自动展开为IN (?, ?, ?),占位符数量与ids.size()严格一致 - 空条件直接跳过:
if (StringUtils.hasText(email)) predicates.add(cb.equal(root.get("email"), email));,不会生成WHERE 1=1 - 模糊匹配只把通配符写死:
cb.like(root.get("email"), email + "%"),email是纯参数,"%"是代码字面量 - 不要写
cb.like(root.get("email"), "%" + email + "%")并允许email包含%或_——那得额外 escape,但更稳妥的是前端约定不传通配符,后端控制
真正容易被绕过的点,不是值怎么绑,而是字段名、排序方向、表关联路径这些“结构部分”有没有做白名单校验。一旦放行任意字符串去驱动 root.get() 或 root.join(),就等于把 SQL 解析权交给了用户输入。











