MyBatis中#{}和${}的区别及底层原理、适用场景和安全风险

 更新时间:2026年04月25日 14:01:21   作者:想不明白的过度思考者  
本文详细解析了MyBatis中#{}和${}的区别及其背后的底层原理、适用场景和安全风险,#{}预编译安全自动转义,适用于业务参数;{**拼接不安全需要手动加引号,主要用于动态表名、列名、排序关键字,使用时须做白名单校验

在 MyBatis 的面试和日常开发中,参数占位符 #{} ${} 的区别是绕不开的核心考点。很多同学只知道“#{} 安全,${} 不安全”,但其背后的底层原理适用场景却一知半解。本文将通过底层分析、日志演示以及 SQL 注入实验,带你彻底搞定这个知识点。

一、 本质区别速览

特性#{}(井号)${}(刀乐/美元符号)
底层原理JDBC 预编译占位符 ?纯字符串拼接,直接替换
SQL 注入安全,自动转义特殊字符危险,容易被恶意篡改
单引号处理自动添加,无需手动处理不自动加,字符串必须手动加 ''
性能高(预编译 SQL 可重复利用)低(每次都需要重新解析)
使用场景99% 的业务参数(Where/Set 值)SQL 结构关键字(表名、排序字段)

二、 核心原理详解

1. #{}:预编译模式(推荐)

当 MyBatis 遇到 #{xxx} 时,它会将 SQL 发送到数据库进行预编译。在执行阶段,再通过 PreparedStatement 设置参数。
1. #{}:预编译模式(推荐)

  • 示例代码
@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 语句中。

  • 示例代码(报错预警):
@Select("select * from user_info where username = ${name}")
UserInfo queryByName(String name);

  • 运行结果:如果你传入 admin,生成的 SQL 是 where username = admin。因为缺少单引号,数据库会报错。
  • 正确写法:必须手动加引号:'${name}'

三、 SQL 注入(必考面试点)

SQL 注入是指攻击者通过在输入框中填入 SQL 片段,篡改原有逻辑的行为。

注入场景:免密登录

假设我们有一段使用 ${} 的危险代码:

SELECT * FROM user WHERE username = '${name}' AND pwd = '${pwd}'

  1. 正常操作:传入用户名 admin,密码 123
  2. 攻击操作:攻击者在用户名框输入 admin' -- ,密码随便写。
  3. 最终生成的 SQL
SELECT * FROM user WHERE username = 'admin' -- ' AND pwd = 'xxx'

在 SQL 中,-- 代表注释。这意味着 AND pwd = ... 的逻辑被直接注销掉了!攻击者无需密码即可直接以管理员身份登录。

结论绝不能使用 ${} 接收用户输入的参数!

四、 SQL 注入场景演示

控制层:UserTestController
注意自己所写的类位置以及包

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

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

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 注入场景演示
接下来访问SQL注⼊的代码:
password设置为' or 1='1
拼接为:http://127.0.0.1:8080/login?name=admin&password=’ or 1='1
注意看网页地址!(百分号和空格进行了URL编码,因为URL不允许直接添加特殊字符)
 SQL 注入场景演示_图2

五、 ${} 的“唯一”合法使用场景

既然 ${} 这么危险,为什么不废掉它?因为它在处理 SQL 结构时无可替代:

  1. 动态表名
    SELECT * FROM ${tableName}#{} 无法用于表名,因为预编译不支持表名占位)。
  2. 动态排序字段
    ORDER BY ${column} ${orderType}(如按 idcreate_time 排序,且指定 ASC/DESC)。

安全建议:在使用这些场景时,必须在 Service 层做白名单校验,确保传入的列名或表名是合法的。

六、 开发规范口诀

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

井号预编译安全自带引号,刀乐拼接危险手动加引号;
业务参数全用井号,结构关键字才用刀乐。

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

到此这篇关于MyBatis中#{}和${}的区别及底层原理、适用场景和安全风险的文章就介绍到这了,更多相关MyBatis中#{}和${}的区别内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Windows环境下安装达梦数据库的完整步骤

    Windows环境下安装达梦数据库的完整步骤

    达梦数据库的安装大致分为Windows和Linux版本,本文将以dm8 企业版 Windows_64位 环境为例,为大家介绍一下达梦数据库的具体安装步骤吧
    2025-03-03
  • 数据库触发器Trigger详解

    数据库触发器Trigger详解

    在数据库管理系统中,触发器(Trigger)是一种特殊的存储过程,它在特定的事件发生时自动执行,本文给大家介绍数据库触发器Trigger的相关知识,感兴趣的朋友一起看看吧
    2025-05-05
  • node-mysql中防止SQL注入的方法总结

    node-mysql中防止SQL注入的方法总结

    大家都知道SQL注入对于网站或者服务器来讲都是一个非常危险的问题,如果这一方面没处理好的话网站可能随时给注入了,所以这篇文章就给大家总结了node-mysql中防止SQL注入的几种常用做法,有需要的朋友们可以参考借鉴。
    2016-10-10
  • 复制数据库表中两个字段数据的SQL语句

    复制数据库表中两个字段数据的SQL语句

    今天为表新添加一个字段,但又想与表中的另一个字段值相同,由于数据过多想通过sql语句实现,经测试下面的这句话确实很好用
    2013-07-07
  • sql join on 用法

    sql join on 用法

    非常不错使用join on实现数据库字段的连接输出效果。
    2009-07-07
  • mybatis映射XML文件详解及实例

    mybatis映射XML文件详解及实例

    这篇文章主要介绍了mybatis映射XML文件详解及实例的相关资料,需要的朋友可以参考下
    2017-03-03
  • 干掉Navicat,这个数据库管理工具真香

    干掉Navicat,这个数据库管理工具真香

    这篇文章主要介绍了干掉Navicat,这个数据库管理工具真香,本文详细的介绍DataGrip的具体使用方法和实现,需要的朋友们下面随着小编来一起学习学习吧
    2020-10-10
  • 使用DBeaver连接PostgreSQL实现方式

    使用DBeaver连接PostgreSQL实现方式

    DBeaver是一款功能强大的数据库管理工具,支持多种数据库,包括PostgreSQL,以下是使用DBeaver连接PostgreSQL的步骤:打开DBeaver,选择创建连接,选择PostgreSQL,输入服务器信息,测试连接,最后点击完成即可
    2025-11-11
  • DBeaver转储数据库报错问题解决办法

    DBeaver转储数据库报错问题解决办法

    DBeaver是一个通用的数据库工具,支持MySQL、PostgreSQL、Oracle、SQLite、SQL Server等多种数据库系统,这篇文章主要介绍了DBeaver转储数据库报错问题的解决办法,需要的朋友可以参考下
    2025-11-11
  • 检查数据库服务器是否正在运行的常见方法小结

    检查数据库服务器是否正在运行的常见方法小结

    在日常的数据库操作和维护中,确保数据库服务器正常运行是至关重要的,本文整理了几种常见的检查数据库服务器是否正在运行的方法,需要的小伙伴可以了解下
    2025-04-04

最新评论