题目
MyBatis中#{}和${}的区别及SQL注入防范
信息
- 类型:问答
- 难度:⭐⭐
考点
参数占位符原理,SQL注入防范,动态SQL使用场景
快速回答
核心区别:
- #{}:预编译处理,自动添加单引号防止SQL注入
- ${}:字符串直接替换,存在SQL注入风险
使用建议:
- 优先使用#{}处理用户输入
- ${}仅用于动态表名/列名等非值场景
- 使用${}时必须手动过滤参数
一、核心原理对比
#{} 工作原理(预编译):
<!-- Mapper示例 -->
<select id="findUser" resultType="User">
SELECT * FROM users WHERE name = #{name}
</select>执行过程:
- MyBatis解析SQL时会将
#{name}替换为? - 通过JDBC PreparedStatement设置参数值
- 最终执行:
SELECT * FROM users WHERE name = ?
${} 工作原理(字符串替换):
<!-- Mapper示例 -->
<select id="findUser" resultType="User">
SELECT * FROM ${tableName} WHERE name = '${name}'
</select>执行过程:
- MyBatis直接拼接SQL:
SELECT * FROM users WHERE name = 'Alice' - 生成完整SQL语句后提交给数据库
二、关键差异总结
| 特性 | #{} | ${} |
|---|---|---|
| 处理方式 | 预编译占位符 | 字符串替换 |
| SQL注入风险 | 安全 | 高危 |
| 参数类型处理 | 自动类型转换 | 需手动处理 |
| 引号添加 | 自动添加 | 需手动添加 |
| 性能 | 预编译可复用执行计划 | 每次重新编译SQL |
三、最佳实践与错误示例
正确用法:
- #{} 用于值传递:
WHERE id = #{userId} - ${} 用于动态标识符:
ORDER BY ${columnName}
危险示例(SQL注入):
// 恶意参数传入
Map<String, Object> params = new HashMap<>();
params.put("orderBy", "name; DROP TABLE users;--");
// Mapper错误写法
<select id="getUsers" resultType="User">
SELECT * FROM users ORDER BY ${orderBy}
</select>执行结果:SELECT * FROM users ORDER BY name; DROP TABLE users;--
四、安全使用${}的方案
方案1:白名单校验
// Service层校验
public List<User> getUsers(String orderBy) {
if(!Arrays.asList("id", "name", "email").contains(orderBy)) {
throw new IllegalArgumentException("Invalid column name");
}
return userMapper.getUsers(orderBy);
}方案2:SqlInjectionFilter工具类
public class SafeSqlUtil {
public static String filterColumn(String input) {
if (!input.matches("[a-zA-Z0-9_]+")) {
throw new IllegalArgumentException("Invalid characters");
}
return input;
}
}
// Mapper中使用
<select id="getUsers" resultType="User">
SELECT * FROM users ORDER BY ${@com.util.SafeSqlUtil@filterColumn(orderBy)}
</select>五、特殊场景处理
动态表名场景:
<select id="getLogs" resultType="Log">
SELECT * FROM logs_${yearMonth}
</select>LIKE查询正确写法:
<!-- 错误:直接使用${} -->
WHERE name LIKE '%${name}%'
<!-- 正确:使用#{}配合CONCAT -->
WHERE name LIKE CONCAT('%', #{name}, '%')
<!-- 正确:使用bind标签 -->
<bind name="pattern" value="'%' + name + '%'" />
WHERE name LIKE #{pattern}六、扩展知识
- 预编译原理:数据库先编译SQL结构,再传入参数值,使注入数据无法改变SQL语义
- MyBatis参数处理:通过
ParameterHandler处理#{},通过SqlSourceBuilder处理${} - OGNL表达式:${}内部可使用OGNL表达式,如
${@java.lang.Runtime@getRuntime().exec('calc')}(需防范表达式注入)
七、常见面试陷阱
问题: "为什么有时用${}排序比#{}快?"
解析: 当排序字段有索引时:
- #{}会产生ORDER BY 'name'(常量排序)
- ${}会产生ORDER BY name(字段排序)
解决方案: 必须用${}但需严格校验参数