题目
MyBatis中#{}和${}的区别及防止SQL注入的最佳实践
信息
- 类型:问答
- 难度:⭐⭐
考点
MyBatis参数占位符,SQL注入防护,动态SQL
快速回答
核心区别:
- #{}:预编译处理,自动添加单引号防止SQL注入
- ${}:字符串直接替换,存在SQL注入风险
最佳实践:
- 普通参数值必须使用#{}占位符
- 动态表名/列名等无法预编译的场景才使用${},并需严格校验参数
- 永远不要用${}处理用户输入值
一、核心原理
#{}工作原理:
- MyBatis会将#{}替换为JDBC的
?占位符 - 通过
PreparedStatement预编译执行,参数值类型安全处理 - 示例SQL:
SELECT * FROM user WHERE name = #{name}→ 实际执行:SELECT * FROM user WHERE name = ?
${}工作原理:
- 直接字符串替换,不做任何转义处理
- 相当于SQL字符串拼接
- 示例SQL:
SELECT * FROM ${tableName}→ 若tableName='user; DROP TABLE user;' 将导致灾难性后果
二、代码示例对比
<!-- 安全示例 -->
<select id="safeQuery" resultType="User">
SELECT * FROM users
WHERE username = #{username} <!-- 预编译安全 -->
</select>
<!-- 危险示例 -->
<select id="dangerousQuery" resultType="User">
SELECT * FROM users
WHERE username = '${username}' <!-- 直接拼接危险! -->
</select>当传入参数:username = "admin' OR '1'='1"
- #{}版本:安全执行,查询用户名为
admin' OR '1'='1的记录 - ${}版本:产生SQL注入,实际执行:
SELECT * FROM users WHERE username = 'admin' OR '1'='1'返回所有数据
三、最佳实践
- 默认使用#{}原则:所有值类型参数(String/Integer/Date等)必须使用#{}占位符
- ${}使用场景限制:
- 动态表名:
SELECT * FROM ${tableName} - 动态列名:
ORDER BY ${columnName} - SQL函数/关键字:
${dateFunc}(NOW())
- 动态表名:
- ${}参数校验规范:
// 在Mapper接口中添加校验 List<User> selectByTable( @Param("tableName") String tableName) { // 白名单校验 Set<String> validTables = Set.of("users", "products"); if(!validTables.contains(tableName)) { throw new IllegalArgumentException("Invalid table name"); } // ... }
四、常见错误
- 错误1:在LIKE语句错误使用${}
<!-- 错误写法 --> WHERE name LIKE '%${keyword}%' <!-- 正确写法 --> WHERE name LIKE CONCAT('%', #{keyword}, '%') - 错误2:IN语句错误使用${}
<!-- 危险写法 --> WHERE id IN (${ids}) <!-- 安全写法 --> WHERE id IN <foreach item="id" collection="ids" open="(" separator="," close=")"> #{id} </foreach>
五、扩展知识
- 底层机制:MyBatis通过
ParameterHandler处理#{},通过SqlSourceBuilder处理${} - 预编译优势:
- 数据库可缓存执行计划提升性能
- 避免数据类型转换错误
- 防止特殊字符(如单引号)导致语法错误
- 特殊场景处理:
- 动态SQL:使用
<if>,<choose>等标签组合#{}实现安全动态查询 - 批量插入:使用
<foreach>配合#{}实现安全批量操作
- 动态SQL:使用