MyBaits中#{}和${}的真正区别,${}的使用场景,#{}如何防止注入?

Posted Java知音_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MyBaits中#{}和${}的真正区别,${}的使用场景,#{}如何防止注入?相关的知识,希望对你有一定的参考价值。

点击关注公众号,利用碎片时间学习

一、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 mysql

net 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语句语法生成的值。

来源:blog.csdn.net/Bb15070047748/article/

details/107188167

推荐

主流Java进阶技术(学习资料分享)

Java面试题宝典

加入Spring技术开发社区

PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。点“在看”支持我们吧!

以上是关于MyBaits中#{}和${}的真正区别,${}的使用场景,#{}如何防止注入?的主要内容,如果未能解决你的问题,请参考以下文章

Hibernate与MyBaits的区别?

mybatis学习

sql 训练及总结

JAVA框架 Mybaits 核心配置

MyBaits基本概念和原理

MyBaits基本概念和原理