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

执行测试代码:

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