#{}和${}的区别
- #{}是预编译处理,${}是字符串替换;
- mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来进行赋值;
- mybatis在处理时,就是把{}替换成变量的值;
- 使用#{}可以有效的防止SQL注入,提高系统的安全性。
SQL注入问题分析
#{}分析
首先来看一个mapper.xml文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mybatis.dao.IUserDao">
<cache></cache>
<select id="findUserByName" resultType="user" useCache="true" parameterType="java.lang.String">
select * from user where name like #{name}
</select>
上述语句在执行的时候,#{id}会被传入的内容替换,替换的时候将传入的内容加上引号当成字符串,例如传入的name为 ’%刘%‘,那么SQL语句在数据库中会这么执行:
select * from user where name like '%刘%';
从log4j可以看到执行过程如下:
2021-01-13 09:06:39,174 486 [ main] DEBUG is.dao.IUserDao.findUserByName - ==> Preparing: select * from user where name like ?
2021-01-13 09:06:39,203 515 [ main] DEBUG is.dao.IUserDao.findUserByName - ==> Parameters: %刘%(String)
2021-01-13 09:06:39,223 535 [ main] DEBUG is.dao.IUserDao.findUserByName - <== Total: 1
User{id=3, name='刘德华', salary=36.65}
从日志可以看到实际上相当于执行了这行代码:
String sql ="select * from user where name like ?";
preparedStatement=connection.preparedStatement(sql);
preparedStatement.setString(1,"%刘%")
调用PreparedStatement的setString方法传入参数可以防止sql注入,安全性更高。PreparedStatement是java.sql包下面的一个接口,用来执行sql语句查询,通过调用connection.preparedStatement(sql)方法可以得到PreparedStatement对象,数据库会对sql语句进行预编译处理,预处理语句将会被预先编译好,这条预编译的sql查询语句能在将来的查询中重用。 说一句人话就是:在使用参数化查询的情况下,数据库系统不会将参数的内容视为sql指令的一部分来处理,而是在数据库完成sql指令编译之后,才能套用参数运行,因此就算参数中包含有破坏性的指令,也不会被数据库所运行。如果只传入一个参数,则名字可以随意取。
${}分析
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mybatis.dao.IUserDao">
<select id="findUserByName" resultType="user" useCache="true" parameterType="java.lang.String">
select * from user where name like '%${name}%'
</select>
${name}会被传入的内容替换掉,这里是所有内容直接替换,不会加上引号。实现代码如下:
package com.mybatis.test;
import com.mybatis.dao.IUserDao;
import com.mybatis.domain.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
public class UserTest {
private InputStream inputStream;
private SqlSessionFactoryBuilder sqlSessionFactoryBuilder;
private SqlSessionFactory sqlSessionFactory;
private SqlSession sqlSession;
private IUserDao iUserDao;
@Before
public void init() throws IOException {
inputStream=Resources.getResourceAsStream("mybatis-config.xml");
sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
sqlSessionFactory = sqlSessionFactoryBuilder.build(inputStream);
sqlSession = sqlSessionFactory.openSession();
iUserDao=sqlSession.getMapper(IUserDao.class);
}
@After
public void destory() throws IOException {
sqlSession.commit();
sqlSession.close();
inputStream.close();
}
//通过姓名模糊查找用户
@Test
public void testFindUserByName(){
sqlSession.clearCache();
List<User> users = iUserDao.findUserByName("' or '1'='1 #name=xxx and password=xxx");
for (User user:users){
System.out.println(user);
}
}
}
例如传入的内容是or ‘1’='1 #name=xxx and password=xxx则sql语句会变为:
select * from user where name like '%' or '1'='1 #name=xxx and password=xxx%'
从log4j看执行过程
2021-01-13 10:20:07,144 521 [ main] DEBUG source.pooled.PooledDataSource - Created connection 1309238149.
2021-01-13 10:20:07,144 521 [ main] DEBUG ansaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4e096385]
2021-01-13 10:20:07,147 524 [ main] DEBUG is.dao.IUserDao.findUserByName - ==> Preparing: select * from user where name like '%' or '1'='1%'
2021-01-13 10:20:07,178 555 [ main] DEBUG is.dao.IUserDao.findUserByName - ==> Parameters:
2021-01-13 10:20:07,200 577 [ main] DEBUG is.dao.IUserDao.findUserByName - <== Total: 4
User{id=2, name='积木', salary=23.56, address='江苏'}
User{id=3, name='刘德华', salary=36.65, address='上海'}
User{id=4, name='积木', salary=23.56, address='湖北'}
User{id=5, name='刘德华', salary=36.65, address='深圳'}
2021-01-13 10:20:07,207 584 [ main] DEBUG ansaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4e096385]
2021-01-13 10:20:07,208 585 [ main] DEBUG ansaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4e096385]
2021-01-13 10:20:07,208 585 [ main] DEBUG source.pooled.PooledDataSource - Returned connection 1309238149 to pool.
可以看到在使用 的 方 式 之 后 , 就 没 有 了 P r e p a r e d S t a t e m e n t 预 编 译 的 过 程 , 向 ′ {}的方式之后,就没有了PreparedStatement预编译的过程,向'% 的方式之后,就没有了PreparedStatement预编译的过程,向′{name}%‘里面传递’ or ‘1’='1 #name=xxx and password=xxx,这样#后面的内容就被注解掉了,这句话就相当于select * from user where name like ‘%’ or ‘1’=‘1%’,即我们在语句中使用${}时,就可以无视name查询出所有的用户信息了,这就是sql注入。