【MyBatis 知识点解析】#{} 与 ${} 的区别及 SQL 注入实战演示
在 MyBatis 的面试和日常开发中,参数占位符 #{} 和 ${} 的区别是绕不开的核心考点。很多同学只知道"#{} 安全,${} 不安全",但其背后的底层原理 和适用场景却一知半解。本文将通过底层分析、日志演示以及 SQL 注入实验,带你彻底搞定这个知识点。
一、 本质区别速览
| 特性 | #{}(井号) | ${}(刀乐/美元符号) |
|---|---|---|
| 底层原理 | JDBC 预编译占位符 ? |
纯字符串拼接,直接替换 |
| SQL 注入 | 安全,自动转义特殊字符 | 危险,容易被恶意篡改 |
| 单引号处理 | 自动添加,无需手动处理 | 不自动加 ,字符串必须手动加 '' |
| 性能 | 高(预编译 SQL 可重复利用) | 低(每次都需要重新解析) |
| 使用场景 | 99% 的业务参数(Where/Set 值) | SQL 结构关键字(表名、排序字段) |
二、 核心原理详解
1. #{}:预编译模式(推荐)
当 MyBatis 遇到 #{xxx} 时,它会将 SQL 发送到数据库进行预编译 。在执行阶段,再通过 PreparedStatement 设置参数。

- 示例代码:
java
@Select("select * from user_info where username = #{name}")
UserInfo queryByName(String name);
- 打印日志(重点) :
通过日志可以观察到,SQL 中参数部分是?占位符:
PREPARE: select * from user_info where username = ? - 优势:由于 SQL 结构已固定,传入的参数只会被当作"值"处理,不会破坏 SQL 语义,从而彻底杜绝 SQL 注入。
2. ${}:字符串拼接模式(慎用)
${} 会在 SQL 执行前,直接把参数原封不动地替换进 SQL 语句中。
- 示例代码(报错预警):
java
@Select("select * from user_info where username = ${name}")
UserInfo queryByName(String name);
- 运行结果 :如果你传入
admin,生成的 SQL 是where username = admin。因为缺少单引号,数据库会报错。 - 正确写法 :必须手动加引号:
'${name}'。
三、 SQL 注入(必考面试点)
SQL 注入是指攻击者通过在输入框中填入 SQL 片段,篡改原有逻辑的行为。
注入场景:免密登录
假设我们有一段使用 ${} 的危险代码:
xml
SELECT * FROM user WHERE username = '${name}' AND pwd = '${pwd}'
- 正常操作 :传入用户名
admin,密码123。 - 攻击操作 :攻击者在用户名框输入
admin' --,密码随便写。 - 最终生成的 SQL:
sql
SELECT * FROM user WHERE username = 'admin' -- ' AND pwd = 'xxx'
在 SQL 中,-- 代表注释。这意味着 AND pwd = ... 的逻辑被直接注销掉了!攻击者无需密码即可直接以管理员身份登录。
结论 :绝不能使用 ${} 接收用户输入的参数!
四、 SQL 注入场景演示
控制层:UserTestController
注意自己所写的类位置以及包
java
import com.example.demo.model.UserInfo;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserTestController {
@Autowired
private UserTestService userService;
@RequestMapping("/login")
public boolean login(String name, String password) {
UserInfo userInfo = userService.queryUserByPassword(name, password);
if (userInfo != null) {
return true;
}
return false;
}
}
业务层:UserTestService
java
import com.example.demo.mapper.UserInfoMapper;
import com.example.demo.model.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserTestService {
@Autowired
private UserInfoTestMapper userInfoMapper;
public UserInfo queryUserByPassword(String name, String password) {
List<UserInfo> userInfos = userInfoMapper.queryUserByPassword(name, password);
if (userInfos != null && userInfos.size() > 0) {
return userInfos.get(0);
}
return null;
}
}
数据层:UserInfoTestMapper
java
import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface UserInfoTestMapper {
@Select("select username, `password`, age, gender, phone from user_info where username= '${name}' and password='${password}' ")
List<UserInfo> queryUserByPassword(String name, String password);
}
启动服务,访问:http://127.0.0.1:8080/login?name=admin&password=admin
程序正常运行

接下来访问SQL注⼊的代码:
password设置为' or 1='1
拼接为:http://127.0.0.1:8080/login?name=admin&password=' or 1='1
注意看网页地址!(百分号和空格进行了URL编码,因为URL不允许直接添加特殊字符)

五、 ${} 的"唯一"合法使用场景
既然 ${} 这么危险,为什么不废掉它?因为它在处理 SQL 结构时无可替代:
- 动态表名 :
SELECT * FROM ${tableName}(#{}无法用于表名,因为预编译不支持表名占位)。 - 动态排序字段 :
ORDER BY ${column} ${orderType}(如按id或create_time排序,且指定ASC/DESC)。
安全建议 :在使用这些场景时,必须在 Service 层做白名单校验,确保传入的列名或表名是合法的。
六、 开发规范口诀
为了方便记忆,我们可以总结为一段口诀:
井号预编译安全自带引号,刀乐拼接危险手动加引号;
业务参数全用井号,结构关键字才用刀乐。
一句话总结:
平时开发无脑用 #{};只有在需要动态传递表名、列名、排序关键字且已经做好安全过滤的情况下,才考虑使用 ${}。

