#{}如何防止SQL注入的?它的底层原理是什么?

article/2025/9/23 12:41:30

一、MyBatis中${}和#{}的区别

1.1 ${}#{}演示

数据库数据:

dao接口:

List<User> findByUsername(String username);List<User> findByUsername2(String username);

Mapper.xml:

<!-- 使用#{} -->
<select id="findByUsername" parameterType="java.lang.String" resultType="com.lscl.entity.User">select * from user where username like #{username}
</select><!-- 使用${},注意${}中的值必须要填value -->
<select id="findByUsername2" parameterType="java.lang.String" resultType="com.lscl.entity.User">select * from user where username like '%${value}%'
</select>

执行测试代码:

@Test
public void findByUsername() throws Exception {InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();SqlSessionFactory factory = builder.build(in);// true:自动提交SqlSession session = factory.openSession(true);UserDao userDao = session.getMapper(UserDao.class);List<User> userList = userDao.findByUsername("%小%");List<User> userList2 = userDao.findByUsername2("小");System.out.println("userList: ");for (User user : userList) {System.out.println(user);}System.out.println("userList2: ");for (User user : userList2) {System.out.println(user);}session.close();in.close();
}

查看执行结果:

发现都能够查询出来

1.2 SQL注入问题

${}会产生SQL注入,#{}不会产生SQL注入问题

我们做一个测试:

List<User> userList2 = userDao.findByUsername2(" aaa' or 1=1 -- ");System.out.println("userList2: ");for (User user : userList2) {System.out.println(user);
}

查询生成的SQL语句:

我们传递的参数是aaa' or 1=1 --,导致查询出来了全部的数据。

大家可以想象一下,如果我是要根据id删除呢?

delete from user where id='${value}'

如果我传递的是:1' or 1=1; --,结果会是什么样,我想大家应该已经知道了。

我这里id是Integer类型,不好测试,就不带大家测试了,大家有兴趣可以自己私下测试。

如果上面使用的是#{}就不会出现SQL注入的问题了

1.3 ${}#{}的区别

#{}匹配的是一个占位符,相当于JDBC中的一个?,会对一些敏感的字符进行过滤,编译过后会对传递的值加上双引号,因此可以防止SQL注入问题。

${}匹配的是真实传递的值,传递过后,会与sql语句进行字符串拼接。${}会与其他sql进行字符串拼接,不能预防sql注入问题。

查看#{}${}生成的SQL语句:

String abc=“123”;#{abc}="123"${value}=123;

1.4 #{}底层是如何防止SQL注入的?

1.4.1 网上的答案

网上关于这类问题非常多,总结出来就两个原因:

1)#{}底层采用的是PreparedStatement,会预编译,因此不会产生SQL注入问题;

其实预编译是MySQL自己本身的功能,和PreparedStatement没关系;而且预编译也不是咱们理解的那个预编译,再者PreparedStatement底层默认根本没有用到预编译(要我们手动开启)!详细往下看

2)#{}不会产生字符串拼接,${}会产生字符串拼接,因此${}会出现SQL注入问题;

这两个答案都经不起深究,最终答案也只是停留在表面,也没人知道具体是为什么。

1.4.2 为什么能防止SQL注入?

我们翻开MySQL驱动的源码一看究竟;

打开PreparedStatement类的setString()方法(MyBatis在#{}传递参数时,是借助setString()方法来完成,${}则不是):

setString()方法全部源码:

public void setString(int parameterIndex, String x) throws SQLException {synchronized(this.checkClosed().getConnectionMutex()) {if (x == null) {this.setNull(parameterIndex, 1);} else {this.checkClosed();int stringLength = x.length();StringBuilder buf;if (this.connection.isNoBackslashEscapesSet()) {boolean needsHexEscape = this.isEscapeNeededForString(x, stringLength);Object parameterAsBytes;byte[] parameterAsBytes;if (!needsHexEscape) {parameterAsBytes = null;buf = new StringBuilder(x.length() + 2);buf.append('\'');buf.append(x);buf.append('\'');if (!this.isLoadDataQuery) {parameterAsBytes = StringUtils.getBytes(buf.toString(), this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor());} else {parameterAsBytes = StringUtils.getBytes(buf.toString());}this.setInternal(parameterIndex, parameterAsBytes);} else {parameterAsBytes = null;if (!this.isLoadDataQuery) {parameterAsBytes = StringUtils.getBytes(x, this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor());} else {parameterAsBytes = StringUtils.getBytes(x);}this.setBytes(parameterIndex, parameterAsBytes);}return;}String parameterAsString = x;boolean needsQuoted = true;if (this.isLoadDataQuery || this.isEscapeNeededForString(x, stringLength)) {needsQuoted = false;buf = new StringBuilder((int)((double)x.length() * 1.1D));buf.append('\'');for(int i = 0; i < stringLength; ++i) {  //遍历字符串,获取到每个字符char c = x.charAt(i);switch(c) {case '\u0000':buf.append('\\');buf.append('0');break;case '\n':buf.append('\\');buf.append('n');break;case '\r':buf.append('\\');buf.append('r');break;case '\u001a':buf.append('\\');buf.append('Z');break;case '"':if (this.usingAnsiMode) {buf.append('\\');}buf.append('"');break;case '\'':buf.append('\\');buf.append('\'');break;case '\\':buf.append('\\');buf.append('\\');break;case '¥':case '₩':if (this.charsetEncoder != null) {CharBuffer cbuf = CharBuffer.allocate(1);ByteBuffer bbuf = ByteBuffer.allocate(1);cbuf.put(c);cbuf.position(0);this.charsetEncoder.encode(cbuf, bbuf, true);if (bbuf.get(0) == 92) {buf.append('\\');}}buf.append(c);break;default:buf.append(c);}}buf.append('\'');parameterAsString = buf.toString();}buf = null;byte[] parameterAsBytes;if (!this.isLoadDataQuery) {if (needsQuoted) {parameterAsBytes = StringUtils.getBytesWrapped(parameterAsString, '\'', '\'', this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor());} else {parameterAsBytes = StringUtils.getBytes(parameterAsString, this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor());}} else {parameterAsBytes = StringUtils.getBytes(parameterAsString);}this.setInternal(parameterIndex, parameterAsBytes);this.parameterTypes[parameterIndex - 1 + this.getParameterIndexOffset()] = 12;}}}

我们执行#{}的查询语句,打断点观察:

最终传递的参数如下:

最终传递的参数为:'aaa\' or 1=1 --

咱们在数据库中执行如下SQL语句(肯定是查询不到数据的):

select * from user where username like 'aaa\' or 1=1 -- '

如果把PreparedStatement加的那根"/"去掉呢?我们执行SQL试试:

select * from user where username like 'aaa' or 1=1 -- '

我们也可以通过MySQL的日志来观察#{}${}产生的SQL语句来分析问题:

1)开启MySQL日志:

在MySQL配置文件中的[mysqld]下增加如下配置:

# 是否开启mysql日志  0:关闭(默认值) 1:开启
general-log=1# mysql 日志的存放位置
general_log_file="D:/query.log"

2)重启MySQL服务(要以管理员身份运行):

net stop mysqlnet start mysql

使用mybatis分别执行如下两条SQL语句:

查看MySQL日志:

1.5 #{}${}的应用场景

既然#{}${}好那么多,那为什么还要有${}这个东西存在呢?干脆都用#{}不就万事大吉吗?

其实不是的,${}也有用武之地,我们都知道${}会产生字符串拼接,来生成一个新的字符串

1.5.1 ${}和#{}用法上的区别

例如现在要进行模糊查询,查询user表中姓张的所有员工的信息

sql语句为:select * from user where name like '张%'

此时如果传入的参数是 “张”

如果使用${}select * from user where name like '${value}%'

生成的sql语句:select * from user where name like '张%'

如果使用#{}select * from user where name like #{value}"%"

生成的sql语句:select * from user where name like '张'"%"

如果传入的参数是 “张%”

使用#{}select * from user where name like #{value}

生成的sql语句:select * from user where name like '张%'

使用${}select * from user where name like '${value}'

生成的sql语句:select * from user where name like '张%'

通过上面的SQL语句我们能够发现#{}是会加上双引号,而${}匹配的是真实的值。

还有一点就是如果使用${}的话,里面必须要填value,即:${value}#{}则随意

1.5.2 什么情况下用${}

场景举例:

代码测试:

执行之后,发现执行成功

我们可以切换一下,把${}改成#{},会出现SQL语法错误的异常

1.6 总结

1.6.1 SQL注入问题

MyBatis的#{}之所以能够预防SQL注入是因为底层使用了PreparedStatement类的setString()方法来设置参数,此方法会获取传递进来的参数的每个字符,然后进行循环对比,如果发现有敏感字符(如:单引号、双引号等),则会在前面加上一个'/'代表转义此符号,让其变为一个普通的字符串,不参与SQL语句的生成,达到防止SQL注入的效果。

其次${}本身设计的初衷就是为了参与SQL语句的语法生成,自然而然会导致SQL注入的问题(不会考虑字符过滤问题)。

1.6.2 #{}${}用法总结 1)#{}在使用时,会根据传递进来的值来选择是否加上双引号,因此我们传递参数的时候一般都是直接传递,不用加双引号,${}则不会,我们需要手动加

2)在传递一个参数时,我们说了#{}中可以写任意的值,${}则必须使用value;即:${value}

3)#{}针对SQL注入进行了字符过滤,${}则只是作为普通传值,并没有考虑到这些问题

4)#{}的应用场景是为给SQL语句的where字句传递条件值,${}的应用场景是为了传递一些需要参与SQL语句语法生成的值。

​ 文末福利
 可以加小新老师vx免费获取【Java高清路线图】和【全套学习视频和配套资料】
​​
 


http://chatgpt.dhexx.cn/article/cQDDOiHO.shtml

相关文章

防止SQL注入的几种方法

一、什么是sql注入 SQL注入是比较常见的网络攻击方式之一&#xff0c;它不是利用操作系统的BUG来实现攻击&#xff0c;而是针对程序员编程时的疏忽&#xff0c;通过SQL语句&#xff0c;实现无帐号登录&#xff0c;甚至篡改数据库。 二、SQL注入攻击的总体思路 1.寻找到SQL注入…

使用Python防止SQL注入攻击

文章目录 文章背景1. 了解Python SQL注入2. 设置数据库2.1 创建数据库2.2 构造数据创建表2.3 设置Python虚拟环境2.4 使用Python连接数据库2.5 执行查询 3. 在SQL中使用查询参数4. 使用Python SQL注入利用查询参数4.1 制作安全查询参数4.2 传递安全查询参数 5. 使用SQL组合6. 结…

php防止sql注入的方法

一.什么是SQL注入式攻击? 所谓SQL注入式攻击&#xff0c;就是攻击者把SQL命令插入到Web表单的输入域或页面请求的查询字符串&#xff0c;欺骗服务器执行恶意的SQL命令。在某些表单中&#xff0c;用户输入的内容直接用来构造(或者影响)动态SQL命令&#xff0c;或作为存储过程的…

如何防止SQL注入攻击?

SQL注入是一种注入攻击&#xff0c;可以执行恶意SQL语句。下面本篇文章就来带大家了解一下SQL注入&#xff0c;简单介绍一下防止SQL注入攻击的方法&#xff0c;希望对大家有所帮助。 什么是SQL注入&#xff1f; SQL注入&#xff08;SQLi&#xff09;是一种注入攻击&#xff0c;…

SecureCRT 7.3软件下载及破解工具+教程

1、首先下载这两个文件&#xff1b; 2、安装scrt7.3.4&#xff0c;我这里提供的是一个32位的版本&#xff0c;64位的机器安装之后也没问题&#xff0c;在安装过程中&#xff0c;程序会提示你是否将32位的软件安装至64的操作系统&#xff0c;直接点击Continue&#xff1b; 3、是…

【51单片机】单片机仿真软件Proteus 8.7破解和汉化教程(附下载地址)

教程 进入安装包P8.7.SP3.exe&#xff0c;接受协议后next 选择Use a locally installed license key&#xff0c;next 进入激活界面&#xff0c;打开Crack包里的license.lxk并安装 默认选项&#xff0c;next 下一步选择完整安装 安装完成后不要运行&#xff0c;点击Close退出安…

软件破解注册码

写在破解之前&#xff1a;&#xff1a;&#xff1a;       软件破解的目的是&#xff1a;有些需要注册的软件&#xff0c;可是找不到注册码&#xff0c;将其破解之后&#xff0c;输入任何注册码都会提示注册成功。             声明&#xff1a;此贴适合从来没接触…

软件破解工具合集

[原文]调试工具&#xff08;Debuggers&#xff09; OllyDbg调试器 OllyDbg v1.1 一个新的动态追踪工具&#xff0c;将IDA与结合起来的思想&#xff0c;Ring 3级调试器&#xff0c;非常容易上手&#xff0c;己代替SoftICE成为当今最为流行的调试解密工具了。强烈推荐&#xff01…

软件破解基础教程

分步阅读 步骤 1 2 3 4 5 6 7 8 先教大家一些基础知识,学习破解其实是要和程序打交道的,汇编是破解程序的必备知识,但有可能部分朋友都没有学习过汇编语言,所以我就在这里叫大家一些简单实用的破解语句吧! -------------------------------------------------…

破解软件

转载&#xff1a;http://gbuy.gdcvi.com/?ArticleID19712 手把手教你破解软件&#xff0c;每一步都有图示&#xff0c;你若再不会就太XX 软件破解的目的是&#xff1a;&#xff1a;&#xff1a;有些需要注册的软件&#xff0c;可是找不到注册码&#xff0c;将其破解之后&am…

软件逆向工程:破解教程(1/24)

软件逆向工程&#xff1a;破解教程&#xff08;1/24&#xff09; 想学破解&#xff0c;花了很多时间还是没有多少进步&#xff1f;网上很多教程&#xff0c;看来看去&#xff0c;到头来还是一头雾水&#xff1f;其实&#xff0c;很正常&#xff0c;不是你的问题&#xff0c;是没…

软件破解初级实例教程(附工具附图)

最近在群里总是看到很多新朋友在问&#xff1a;1、“新手怎么学破解啊&#xff1f;”&#xff08;这是标准的伸手党&#xff0c;baidu google其实很好用&#xff09;2、“哎呀XX大牛&#xff0c;我什么基础都没有啊我不会汇编&#xff0c;不会C更不会C还不会…………总之高手会…

软件破解实例教程

破解需要的软件&#xff08;点击下载&#xff09;&#xff1a; 侦壳 language.exe 脱壳AspackDie.exe 反编译 W32Dasm黄金中文版 16进制编辑器 UltraEdit.rar 在破解之前先复习一下基础知识&#xff1a; 一.破解的等级 初级,修改程序,用ultraedit修改exe文件,称…

原码 反码 补码的简单计算附例题

原码 反码 补码 对计算机中常见数据简单分类机器数 与 真值原码反码补码扩展 对计算机常见数据的分类 机器数 就是数值在计算机中的二进制表现形式 机器数在计算机中有符号,使用 最高位表示符号 , 使用0 表示正,使用 1 表示负 一个字节8个bit位表示 5 0000 0101 -10 1…

原码反码补码移码

原码&#xff1a; 数值直接转为二进制数&#xff0c;负数的最高位 置1。 以8位为例&#xff1a; 1的原码为0000 0001&#xff0c;-1的原码为1000 0001. 127的原码为0111 1111&#xff0c;-127的原码为1111 1111. 0的原码为0000 0000&#xff0c;-0的原码为1000 0000. 反码…

二进制原码反码补码详解

二进制原码反码补码 首先我们在了解什么是原码&#xff0c;反码&#xff0c;补码之前&#xff0c;我们先来谈谈为什么需要有这些&#xff0c;只要原码不行吗&#xff1f; 答案肯定是不行的&#xff01;&#x1f61c; 因为在计算机中&#xff0c;二进制的运算对于正数之间不会出…

真值 原码 反码 补码

引言 计算机中只能做加法运算&#xff0c;它的减法是通过加法来实现的。原码&#xff0c;反码&#xff0c;补码的产生过程&#xff0c;就是为了解决计算机做减法和引入符号位的问题 真值 正数或负数的真值 → 为其绝对值对应的二进制数前面加上正号或负号 例如&#xff1a; 1 …

原码 反码 补码及应用

原码 反码 补码及应用 原码 什么是原码&#xff1f; 原码&#xff1a;十进制数据的二进制表现形式&#xff0c;最左边是符号位&#xff0c;0为正&#xff0c;1为负。 56 > 0 0111000 ​ 符号位 数据 最大值&#xff1a;01111111 > 127 最小值&#xff1a;11111111…

原码反码补码原理理解

原码反码补码原理理解 基础知识原码定义在这里&#xff0c;我们模仿一下计算机对数据运算的过程&#xff01;那么所有运算都是正确的吗&#xff1f;我们再试一组。 反码定义在这里&#xff0c;我们模仿一下计算机对数据运算的过程&#xff01;正数加负数也没问题了&#xff0c;…

java基础-原码反码补码

本文帮助理解&#xff0c;Java中原码反码补码的原理 1&#xff1a;原码反码补码&#xff0c;基础概念和计算方法 对于一个数&#xff0c;计算机需要使用一定的编码方式进行存储。原码反码补码是计算机存储一个具体数字的编码方式。 原码&#xff1a; 第一位表示符号位&…