mybatis中#{}和${}的区别

首先我们看看mybatis本身的说明:

String Substitution

By default, using the #{} syntax will cause MyBatis to generate PreparedStatement properties and set the values safely against the PreparedStatement parameters (e.g. ?). While this is safer, faster and almost always preferred, sometimes you just want to directly inject a string unmodified into the SQL Statement. For example, for ORDER BY, you might use something like this:

ORDER BY ${columnName}
Here MyBatis won't modify or escape the string.

NOTE It's not safe to accept input from a user and supply it to a statement unmodified in this way. This leads to potential SQL Injection attacks and therefore you should either disallow user input in these fields, or always perform your own escapes and checks.

 

从上文可以看出:

1. 使用#{}格式的语法在mybatis中使用Preparement语句来安全的设置值,执行sql类似下面的:

PreparedStatement ps = conn.prepareStatement(sql);
ps.setInt(1,id);

这样做的好处是:更安全,更迅速,通常也是首选做法。

2. 不过有时你只是想直接在 SQL 语句中插入一个不改变的字符串。比如,像 ORDER BY,你可以这样来使用:

ORDER BY ${columnName}

此时MyBatis 不会修改或转义字符串。

这种方式类似于:

   Statement st = conn.createStatement();
       
      ResultSet rs = st.executeQuery(sql);

这种方式的缺点是: 以这种方式接受从用户输出的内容并提供给语句中不变的字符串是不安全的,会导致潜在的 SQL 注入攻击,因此要么不允许用户输入这些字段,要么自行转义并检验。

 

 

 

小结:

Mybatis本身是基于JDBC封装的。

#{para}是预编译处理(PreparedStatement)范畴的。

${para}是字符串替换。

Mybatis在处理#时,会调用PreparedStatement的set系列方法来赋值;处理$时,就是把${para}替换成变量的值。

使用#{para}可以有效的防止SQL注入,提高系统安全性。



一、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语句:


 
查询生成的SQL语句

我们传递的参数是aaa' or 1=1 --,导致查询出来了全部的数据。大家可以想象一下,如果我是要根据id删除呢?

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

如果我传递的是:1' or 1=1; --,结果会是什么样,我想大家应该已经知道了。如果上面使用的是#{}就不会出现SQL注入的问题了

 
使用#{}
1.3 ${}和#{}的区别

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

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

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

 
查看#{}和${}生成的SQL语句
String abc=“123”;

#{abc}="123"

${value}=123;
1.4 为什么能防止SQL注入?

我们翻开MySQL驱动的源码一看究竟;打开PreparedStatement类的setString()方法(MyBatis在#{}传递参数时,是借助setString()方法来完成,${}则不是):

 
PreparedStatement

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 -- '
 
根"/"去掉
1.5 #{}和${}的应用场景

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

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

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

例如现在要进行模糊查询,查询user表中姓张的所有员工的信息
sql语句为:select * from user where name like '张%'

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

如果使用${}:

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 '张'"%"

2、如果传入的参数是 “张%”
使用#{}: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语句我们能够发现#{}是会加上双引号,而${}匹配的是真实的值。

1.5.2 什么情况下用${}?

场景举例:


 
场景举例

代码测试:

 
代码测试

执行之后,发现执行成功

 
执行结果

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


 
异常

二、总结

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

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

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

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

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

三、项目中使用like

MyBaits中用#{}防止注入

方式一
<if test="params.searchKey != null and params.searchKey != ''">
   and client_id like concat('%', #{params.searchKey}, '%')
</if>
方式二
1、查询条件对象
@Data
@NoArgsConstructor
@AllArgsConstructor
@SuppressWarnings("serial")
@ApiModel(value = "QueryLuckyActivityVO")
public class QueryLuckyActivityVO {

    @ApiModelProperty(value = "抽奖活动ID")
    private String activityNo;

    @ApiModelProperty(value = "抽奖活动标题")
    private String activityName;

    @ApiModelProperty(value = "参与人数(大于)")
    private Integer joinNumberStart;

    @ApiModelProperty(value = "参与人数(小于)")
    private Integer joinNumberEnd;

    @ApiModelProperty(value = "状态")
    private PublishStatusEnum publishStatus;

    @ApiModelProperty(value = "活动类型(1=抽奖+兑奖;2=仅抽奖;3=仅兑奖)")
    private String activityType;

    public String getActivityNo() {
        if(StringUtil.isNotBlank(activityNo)){
            return "%"+activityNo+"%";
        }
        return activityNo;
    }

    public String getActivityName() {
        if(StringUtil.isNotBlank(activityName)){
            return "%"+activityName+"%";
        }
        return activityName;
    }
}
2、mybatis
<select id="findPage" resultType="com.alanchen.lucky.dto.LuckyActivityDTO">
        select * from   t_lucky_activity m
        <where>
            m.deleted = 0
            <if test="vo.activityNo != null and vo.activityNo!=''">
                and m.activity_no like #{vo.activityNo}
            </if>
            <if test="vo.activityName != null and vo.activityName!=''">
                and m.activity_name like #{vo.activityName}
            </if>
            <if test="vo.joinNumberStart != null">
                and m.join_number <![CDATA[ >= ]]> #{vo.joinNumberStart}
            </if>
        </where>
        order by m.create_time desc
    </s


 

posted on 2018-05-06 17:15  一只阿木木  阅读(2856)  评论(0编辑  收藏  举报