本文测试的表数据(这只是该表部分数据)
StudentMapper.java
@Mapper
public interface StudentMapper {
List<Student> getByName(@Param("stu") Student student);
}
先分析一条sql语句的占位符全是#{}的情况
mapper.xml查询语句
<select id="getByName" resultType="com.kkb.pojo.Student" parameterType="com.kkb.pojo.Student">
SELECT
id as id,
`name` as name,
age as age,
sex as sex
FROM
student
WHERE
`name` = #{stu.name}
</select>
单元测试:
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {
@Test
public void getByNameTest() {
Student student = new Student();
student.setName("'' or 1=1 or name=''");
List<Student> byName = studentService.getByName(student);
System.out.println(JSON.toJSONString(byName));
}
}
源码执行的过程:
org.apache.ibatis.executor.SimpleExecutor#doQuery
—>org.apache.ibatis.executor.SimpleExecutor#prepareStatement
—>org.apache.ibatis.executor.statement.StatementHandler#prepare
—>org.apache.ibatis.executor.statement.BaseStatementHandler#prepare
—>org.apache.ibatis.executor.statement.PreparedStatementHandler#instantiateStatement
观察执行的sql
如图:最终是在PreparedStatementHandler#instantiateStatement
方法中通过connection.prepareStatement(sql)
来执行boundSql.getSql()
得到的sql语句:
SELECT
id as id,
`name` as name,
age as age,
sex as sex
FROM
student
WHERE
`name` = ?
最终执行的sql:
SELECT id as id, `name` as name, age as age, sex as sex
FROM student
WHERE `name` = '' or 1=1 or name='';
执行的结果:
-
按道理,
sql
的条件里面有or 1=1
会导致将上面的表里面的所以数据查询出来,但是这里没有,这里查询的结果是没有一条符合这个sql查询条件的记录,这是为什么呢? -
个人的理解是因为入参的占位符用的是#{},导致前面的代码
org.apache.ibatis.executor.statement.PreparedStatementHandler#instantiateStatement
通过boundSql.getSql();
拿到的sql
是将#{}
占位符替换为问号的"?",然后通过prepareStatement
对象对这个sql
进行预编译(预编译代表这个sql
的结构已经编译固定了,后面再填充进来的sql
关键字不会当做sql
的关键字处理,而是普通的字符串值),最终参数填充只是作为值来替换掉问号"?". 所以,这里的name
条件设置的是name='' or 1=1 or name=''
, 但其中的or 1=1
不会当做被当or条件处理,它只是不同的字符串值。在这个表中,因为没有name='' or 1=1 or name=''
的记录,所以查询的结果没有数据。 -
可以看出通过使用
#{}
作为参数的占位符时,会先将sql
中#{}
的占位符替换为问好"?",然后通过prepareStatement
对象对sql
进行预编译,最后用其他过程来填充入参。因此可以知道使用#{}
作为占位符的参数不会出现sql
注入的问题。
看一下这里的connection和Statement对象
- 可以看出
connection
是一个代理对象,而它的h属性是ConnectionLogger
,并且h
属性中的connection
属性的对象是DruidPooledConnection
,这是因为我这个项目里面用的数据库连接池是阿里的druid
,所以这里的connection
最终是DruidPooledConnection
这个连接池里面的connection
。
扯一点题外话,通常以$ProxyXxx
开头的类基本上就知道这个类是通过JDK动态代理运行时动态编译出来的代理类的对象,而这个对象中的h属性肯定是InvocationHandler接口的实现类,而这里的ConnectionLogger
里面的connection
属性我认为就是$Proxy100@7151
代理对象代理的目标对像。
分析一条sql语句的占位符全是${}的情况
mapper.xml查询语句
- 占位符改为${}
<select id="getByName" resultType="com.kkb.pojo.Student" parameterType="com.kkb.pojo.Student">
SELECT
id as id,
`name` as name,
age as age,
sex as sex
FROM
student
WHERE
`name` = ${stu.name}
</select>
单元测试:
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {
@Test
public void getByNameTest() {
Student student = new Student();
student.setName("'' or 1=1 or name=''");
List<Student> byName = studentService.getByName(student);
System.out.println(JSON.toJSONString(byName));
}
}
源码执行的过程(和上述一样):
org.apache.ibatis.executor.SimpleExecutor#doQuery
—>org.apache.ibatis.executor.SimpleExecutor#prepareStatement
—>org.apache.ibatis.executor.statement.StatementHandler#prepare
—>org.apache.ibatis.executor.statement.BaseStatementHandler#prepare
—>org.apache.ibatis.executor.statement.PreparedStatementHandler#instantiateStatement
观察执行的sql
-
从这里就可以看出占位符使用
${}
和#{}
的区别:用#{}
的话,就是先将占位符#{}
替换为问号"?",然后使用prepareStatement
对带有问号的sql
进行预编译,最后参数填充执行最终的sql
。而用${}
的话,则会直接将参数替换${}
占位符,然后直接通过prepareStatement
执行参数填充后的sql
。 -
#{}
:会先通过prepareStatement
预编译sql
,再进行参数填充,执行最终的sql
。 -
${}
:会直接用参数替换${}
占位符,然后用prepareStatement
执行参数填充后的sql
,该过程没有sql
语句的预编译。所以这里使用${}
占位符,会存在sql
注入的问题,因为name
值里面注入了or 1=1
,所以会导致所以的数据被查询出来。
最终执行的sql
SELECT id as id, `name` as name, age as age, sex as sex
FROM student
WHERE `name` = ''or 1=1 or name='';
执行结果
- 从图中可以看到查询的结果是1000573行记录。证实了
${}
会导致sql
注入的问题。
看一下这里的connection和Statement对象
- 这里和上面
#{}
占位符的情况一样
分析一条sql语句的占位符同时包含${}和#{}的情况
mapper.xml查询语句
<select id="getByName" resultType="com.kkb.pojo.Student" parameterType="com.kkb.pojo.Student">
SELECT
id as id,
`name` as name,
age as age,
sex as sex
FROM
student
WHERE
`name` = ${stu.name}
and age = #{stu.age}
</select>
单元测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {
@Test
public void getByNameTest() {
Student student = new Student();
student.setName("'' or 1=1 or name=''");
student.setAge(1);
List<Student> byName = studentService.getByName(student);
System.out.println(JSON.toJSONString(byName));
}
}
源码执行过程
org.apache.ibatis.executor.SimpleExecutor#doQuery
—>org.apache.ibatis.executor.SimpleExecutor#prepareStatement
—>org.apache.ibatis.executor.statement.StatementHandler#prepare
—>org.apache.ibatis.executor.statement.BaseStatementHandler#prepare
—>org.apache.ibatis.executor.statement.PreparedStatementHandler#instantiateStatement
观察执行的sql
- 可以看出当一条sql里面的占位符
#{}
和${}
都存在时,#{}
占位符依然会被问号"?“替换,${}
占位符则直接被入参替换,然后通过prepareStatement
对象对sql
预编译,最后用对应的参数替换问号”?",因此得出的结论是:[当一条sql里面的占位符#{}
和${}
都存在时,#{}
作为占位符的入参不会导致sql
注入的问题,而${}
作为占位符的入参依然会导致sql注入的问题]
SELECT
id as id,
`name` as name,
age as age,
sex as sex
FROM
student
WHERE
`name` = ''or 1=1 or name=''
and age = ?
最终执行的sql
SELECT id as id, `name` as name, age as age, sex as sex
FROM student
WHERE `name` = ''or 1=1 or name='' and age = 1;
执行的结果
看一下这里的connection和Statement对象
- 这里和上面
#{}
占位符的情况一样
解惑
通过这次的研究也纠正了我之前的错误理解。
之前我的理解是:
- 通过
${}
作为占位符的话,sql
就是通过Statement
对象来处理sql
的 - 通过
#{}
作为占位符的话,sql
就是通过PrePareStatement
对象来处理sql
的
现在知道这样理解是错误的,其实它都是通过PrePareStatement
来处理sql
的,只是如果参数的占位符是#{}
的话,则这个参数在预编译的时候是用问号"?"替换了#{}
占位符;如果参数的占位符是${}
的话,则这个参数在预编译的时候是用参数的值替换了${}
占位符,然后最终预编译之后,再用原来#{}
占位符上的参数值来填充对应的参数。所以#{}
不会有sql
注入的问题,而${}
会有sql
注入的问题。