---JavaEE专栏---

【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 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 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 层做白名单校验,确保传入的列名或表名是合法的。

六、 开发规范口诀

为了方便记忆,我们可以总结为一段口诀:

井号预编译安全自带引号,刀乐拼接危险手动加引号;

业务参数全用井号,结构关键字才用刀乐。

一句话总结:

平时开发无脑用 #{};只有在需要动态传递表名、列名、排序关键字且已经做好安全过滤的情况下,才考虑使用 ${}。