使用过mybatis的程序员应该都知道,在写sql时,遇到变量时会用 #{变量名}, 也知道这样可以防止sql注入,今天看看原理,做个笔记(主看最中心原理)。
首先说结论:mybatis底层依赖的是 java.sql.PreparedStatement 实现防注入的。
图0 官方文档部分截图
在这里调用了 java.sql.Connection.prepareStatement 方法创建 PrepareStatement, 这样可以把sql发送给数据库, 数据库会对sql的进行预编译,
(
直白一点, sql预编译后,就不会发生sql注入的情况,例如:
select * from student where id = #{num, jdbctype = VARCHAR}
经过预编译后,把
num = 'no1'; delete from student where 1=1; 传入到sql中
这时数据库就会认为 'no1'; delete from student where 1=1; 是一个字符串,也就是把内部的特殊字符转义了,即
select * from student where id = '\'no1\'; delete from student where 1=1;'
而不是结合后的两个sql
select * from student where id = 'no1'; delete from student where 1=1;
)
mybatis源码关于防止注入部分可以分两部分;
1. 解析sql
-
执行sql
1. 解析sql
图1
根据图1,mybatis在扫描到sql后,会把#{} 从sql字符串中替换成 ?,同时也会根据接口方法的参数类型来记录sql的参数类型。
2. 执行sql
图2
根据图2,当执行mapper接口中的方法时,会根据方法的全路径名匹配到sql,匹配成功后,会将sql(图2中的params[0])传入到 java.sql.Connection.prepareStatement 方法中,这样就实现了sql的预编译。
图3
根据图3,在向sql中注入变量值时,mybatis会根据 Mapper中的方法参数类型和值,最终映射到 PreparedStatement 中的 setInt、setString。。等方法设置sql参数。(例如图3调用的是getSubject(final int id)方法,所以最终会调用PreparedStatement的setInt方法)。设置完参数后提交声明到数据库。
以上就是mybatis防注入的原理和简要流程。
3. 如果sql使用 ${}时;
图4
根据图4,当sql使用 select * from subject where id = ${id},则在调用mapper方法时, 会直接把sql中的占位符替换成变量,因为这里是直接进行替换,而没有预编译,所以会有sql注入的风险。
4. 如果sql一起使用 #{} 和 ${} 时
在解析sql阶段时,直接按照${} 不对sql进行处理
图5
根据图5,在执行sql阶段,会先解析${},再解析#{}
图6 先解析${}
图7 后解析#{}
5. 综合以上:
mybatis对代码中的sql语句的处理步骤:
5.1 在初始化阶段,
先扫描{},则继续处理#{},并将#{}替换成 ?
5.2 执行sql阶段
5.2.1 扫描{} 替换成相应的变量值(因为是字符串替换,所以这里就存在sql注入问题),否则跳过本步骤;
5.2.2 扫描#{}, 如果存在(只有当${} 和 #{}同时存在一个sql中时),则将#{}替换成?,否则跳过本步骤
5.2.3 (如果5.2.2存在)设置变量值, 使用PreparedStatement 执行sql