适用读者:所有写
@Select的人——这一篇能防住一类真实漏洞。
前置:S08-B01。
实测环境:MyBatis 3.0.3 + H2(2026-11-05)。
覆盖旧文:896(59) 898(77) + 空壳 1137 补写 = 136 点。
覆盖扩展:E-S08-003(#{}预编译 vs${}拼接)、E-S08-010(空壳 1137 SQL 预编译,已补写)。
上一篇:S08-B01 · Mapper 为什么不用写实现类
下一篇:S08-B03 · 动态 SQL。
1. 一句话结论
#{} 走 JDBC 预编译占位符(?),${} 是字符串直接拼接进 SQL。前者安全,后者一旦接外部输入就是注入漏洞。
2. 实测:注入串的两种结果
@Select("select * from sys_user where user_name = '${name}'") // 拼接
List<User> byNameUnsafe(@Param("name") String name);
@Select("select * from sys_user where user_name = #{name}") // 预编译
List<User> byNameSafe(@Param("name") String name);
正常输入 alice:
2. #{} 传 name=alice → [User(1,alice,20)]
3. ${} 传 name=alice → [User(1,alice,20)]
注入串 x' OR '1'='1:
4. ${} 传注入串 → 条数 3 ← 返回全表!
5. #{} 传注入串 → 条数 0 ← 安全
| 写法 | 拼出的 SQL | 结果 |
|---|---|---|
${} |
... where user_name = 'x' OR '1'='1' |
3 行(全表泄露) |
#{} |
... where user_name = ?(参数 x' OR '1'='1) |
0 行 |
这是本篇最重要的一段:不是"可能被注入",是一个字符串就把全表查出来了。
3. ${} 什么时候必须用
| 场景 | 说明 |
|---|---|
| 动态表名/列名 | order by ${column}、from ${table} |
| 排序方向 | order by ${dir}(只允许 asc/desc) |
| 已知枚举拼接 | 数量固定的分类条件 |
使用纪律:${} 的内容必须来自"白名单",绝不能来自用户输入。
// 正确:白名单校验后再拼接
String col = ALLOWED.contains(column) ? column : "id";
4. 结果映射:驼峰与 ResultMap
实测:user_name 列自动映射到 userName 字段(配置 map-underscore-to-camel-case=true 后):
2. #{} 传 name=alice → [User(1,alice,20)]
| 方式 | 写法 | 适用 |
|---|---|---|
| 自动映射 | 列名 snake_case ↔ 字段 camelCase |
约定一致时 |
@Results / XML resultMap |
显式映射 | 列名与字段不同、需要类型转换 |
| 别名 | select user_name as userName |
简单场景 |
判据:"列名和字段名能对上吗?" 能 → 开驼峰自动映射;不能 → 显式 ResultMap。
5. 参数传递的四种写法
| 写法 | 说明 |
|---|---|
| 单个基本类型 | #{任意名} 都行(只有一个参数) |
@Param |
多参数必须用 |
| POJO | #{fieldName} |
Map |
#{key} |
坑:多参数不加 @Param,MyBatis 会用 arg0/param1 命名→ #{name} 报 BindingException。
6. 五个误区
| 误区 | 真相 |
|---|---|
"${} 只是性能更好" |
是拼接,可注入 |
| "参数化查询不会被注入" | 只有 #{} 才是参数化 |
| "驼峰映射默认开启" | 需配置(Spring Boot 下可 mybatis.configuration.map-underscore-to-camel-case) |
"多参数不用 @Param" |
会 BindingException |
"${} 完全不能用" |
动态表名/排序列需要,但要白名单 |
7. 自检
#{}与${}在 JDBC 层分别变成什么?- 实测里注入串在
${}下返回几行?为什么? ${}的合法用途有哪些?纪律是什么?- 驼峰映射怎么开?
- 多参数不加
@Param会怎样?
8. 下一篇
S08-B03 · 动态 SQL:<if>/<where>/<foreach>
注讲座完,下一篇讲"条件拼接"的正确做法——用标签而不是字符串拼。
本篇属于系列《S08 · MyBatis 持久层》。旧文来源:A-896/898(空壳 1137 补写);扩展点 E-S08-003。注入对照为 MyBatis 3.0.3 + H2 实测。