一、核心概念
在 MyBatis 的 XML 映射文件中,有两种方式将参数拼接到 SQL 语句中:
${}
:直接字符串拼接(文本替换),不处理特殊字符,存在 SQL 注入风险。#{}
:预编译占位符(?
),自动过滤特殊字符,能防止 SQL 注入。
二、代码示例分析
以下是一个模糊查询的 MyBatis XML 配置和测试用例:
1. XML 映射文件
<select id="findByNameLike2" resultType="Emp">
select * from emp where name like '${name}'
</select>
2. 测试用例
@Test
public void testFindByNameLike2() throws IOException {
// 正常查询:传入 "%张%"
mapper.findByNameLike2("%张%").forEach(System.out::println);
// 生成SQL:select * from emp where name like '%张%'
// 恶意输入:传入 "abc' or '1=1----"
mapper.findByNameLike2("abc' or '1=1----").forEach(System.out::println);
// 生成SQL:select * from emp where name like 'abc' or '1=1----'
}
三、${}
的 SQL 注入风险详解
1. 正常输入场景
- 参数:
"%张%"
- 生成 SQL:
select * from emp where name like '%张%'
- 效果:正确查询名字包含“张”的员工。
2. 恶意输入场景
- 参数:
"abc' or '1=1----"
- 生成 SQL:
select * from emp where name like 'abc' or '1=1----'
- 解析:
由于'1=1'
是恒真条件,此 SQL 会返回所有员工数据,导致数据泄露。
四、#{}
的预编译机制
1. 使用 #{}
的修改版
<select id="findByNameLikeSafe" resultType="Emp">
select * from emp where name like #{name}
</select>
2. 测试用例对比
@Test
public void testFindByNameLikeSafe() {
// 正确用法:传入 "%张%"
mapper.findByNameLikeSafe("%张%");
// 生成SQL:select * from emp where name like ?
// 恶意输入:传入 "abc' or '1=1----"
mapper.findByNameLikeSafe("abc' or '1=1----");
// 生成SQL:select * from emp where name like ?
// 参数值:被转义为 "abc' or '1=1----"(视为普通字符串)
}
- 效果:恶意参数会被视为整体字符串,无法改变 SQL 结构。
五、使用建议与最佳实践
1. 优先使用 #{}
- 所有用户输入或外部参数必须使用
#{}
,尤其是 WHERE 条件中的值。
2. 谨慎使用 ${}
- 适用场景:动态表名、动态列名等非用户输入场景。
- 风险控制:若必须使用
${}
,需手动过滤危险字符(如单引号'
)。
3. 模糊查询的正确写法
- 安全方式:在参数中拼接
%
,而非 SQL 中。
// Java 代码中处理
String name = "%" + userInput + "%";
mapper.findByNameLikeSafe(name);
六、总结
特性 | ${} |
#{} |
---|---|---|
处理方式 | 直接替换 | 预编译占位符 |
SQL 注入风险 | 高风险 | 无风险 |
适用场景 | 动态表名/列名 | 用户输入、条件值 |
性能 | 无额外解析 | 预编译优化 |
始终遵循规则:用户输入必须通过 #{}
传递!