1.MyBatis是一个ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动,创建连接,创建statement等复杂的过程。开发人员不需要编写原生态sql,可以严格控制sql执行性能,灵活度高。
2.MyBatis可以使用xml或者注解来配置映射原生信息,将P0J0映射成数据库中的记录,避免了几乎所有的JDBC代码和手动设置的参数以及获取结果集。
接下来要讲解的是Mybatis的诸多特性。包括其执行流程,延迟加载和二级缓存,以及在Mybatis中预防sql注入的问题。
执行流程
总结:
1)读取MyBatis配置文件:mybatis-config.xml加载运行环境和映射文件
2)构造会话工厂SqlSessionFactory
3)会话工厂创建SqlSession对象(包含了执行SQL语句的所有方法)
4)操作数据库的接口,Executor执行器,同时负责查询缓存的维护
5)Executor接口的执行方法中有一个MappedStatement类型的参数,封装了映射信息
6)输入参数映射
7)输出结果
延迟加载
Mybatis是否支持延迟加载?
-
延迟加载的意思是:就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。
-
Mybatis支持一对一关联对象和一对多关联集合对象的延迟加载
-
在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=truelfalse,默认是关闭的
延迟加载的底层原理知道吗?
1.使用CGLIB创建目标对象的代理对象
2.当调用目标方法时,进入拦截器invoke方法,发现目标方法是null值,执行sql查询
3.获取数据以后,调用set方法设置属性值,再继续查询目标方法,就有值了
二级缓存
当然,请求总是会先查看缓存的,没有再去执行sql。本地缓存分为一级和二级,查询顺序是:二级->一级->数据库。可以以这张图作为参考:
接下来依次讲解一级与二级缓存:
-
一级缓存:基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当Session进行flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存
-
二级缓存是基于namespace和mapper的作用域起作用的,不是依赖于SQLsession,默认也是采用PerpetualCache,HashMap 存储。需要单独开启,一个是核心配置,一个是mapper映射文件
二级缓存注意事项:
1.对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了新增、修改、删除操作后,默认该作用域下所有 select 中的缓存将被 clear
2.二级缓存需要缓存的数据实现Serializable接口
3.只有会话提交或者关闭以后,一级缓存中的数据才会转移到二级缓存中
PerpetualCache的实现其实就是对HashMap进行了一层封装。
sql 注入
原文链接:https://www.cnblogs.com/2ha0yuk7on/p/16880528.html#tid-iMDt3x,此处为全文搬运转载,侵删。
1、概述
SQL 注入(SQL Injection)是发生在 Web 程序中数据库层的安全漏洞,是网站存在最多也是最简单的漏洞。主要原因是程序对用户输入数据的合法性没有判断和处理,导致攻击者可以在 Web 应用程序中事先定义好的 SQL 语句中添加额外的 SQL 语句,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步获取到数据信息。
简单地说,就是通过在用户可控参数中注入 SQL 语法,破坏原有 SQL 结构,达到编写程序时意料之外结果的攻击行为。其成因可以归结为如下原因造成的:
-
程序编写者在处理应用程序和数据库交互时,使用字符串拼接的方式构造 SQL 语句。
-
且未对用户可控参数进行足够的过滤。
2、漏洞复现
下面使用DVWA靶场来进行演示,网站架构为PHP,我们重点关注漏洞原理即可。
该页面提供了一个简单的查询功能,可以根据前端输入的用户ID来查询对应的用户信息。如图,输入 1
,返回了对应 admin 用户的信息。
img
查看该页面的源代码:
<?php
if( isset( $_REQUEST[ 'Submit' ] ) ) {
// Get input
$id = $_REQUEST[ 'id' ];
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysqli_query($GLOBALS["___mysqli_ston"], $query ) or die( '<pre>' . ((is_object($GLOBALS["___mysqli_ston"])) ? mysqli_error($GLOBALS["___mysqli_ston"]) : (($___mysqli_res = mysqli_connect_error()) ? $___mysqli_res : false)) . '</pre>' );
// Get results
while( $row = mysqli_fetch_assoc( $result ) ) {
// Get values
$first = $row["first_name"];
$last = $row["last_name"];
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
}
mysqli_close($GLOBALS["___mysqli_ston"]);
}
?>
进行代码审计可以发现,程序将前端输入的 id
参数未加任何处理,直接拼接在了 SQL 语句中,那么此时就导致了SQL注入漏洞。
若此时攻击者输入的用户ID为 1' or 1='1
,则程序拼接后执行的 SQL 语句变成了:
SELECT first_name, last_name FROM users WHERE user_id = '1' or 1='1';
可见,攻击者通过单引号 '
闭合了数据库查询语句,并且在查询条件之后构造了“或 1=1”,即“或真”的逻辑,导致查询出了全部用户的数据。
如果攻击者可以任意替代提交的字符串,就可以利用 SQL 注入漏洞改变原有 SQL 语句的含义,进而执行任意 SQL 命令,入侵数据库进行脱库、删库,甚至通过数据库提权获取系统权限,造成不可估量的损失。(SQL注入的场景类型非常之多,攻击手法、绕过姿势也非常多,本文不作重点讨论)
3、修复建议
一般来说,防御 SQL 注入的最佳方式就是使用预编译语句(其他防御方法还有很多,本文不作重点讨论),绑定变量。例如:
String sql = "SELECT * FROM user_table WHERE username = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, "zxd");
ResultSet results = pstmt.executeQuery();
使用预编译的 SQL 语句,SQL 语句的语义不会发生改变。在 SQL 语句中,变量用占位符 ?
表示,攻击者无法改变 SQL 的结构。
4.Mybatis 框架简介
参数符号的两种方式
Mybatis 的 SQL 语句可以基于注解的方式写在类方法上面,更多的是以 xml 的方式写到 xml 文件。Mybatis 中 SQL 语句需要我们自己手动编写或者用 generator 自动生成。编写 xml 文件时,MyBatis 支持两种参数符号,#{}
和 ${}
。
-
#{}
使用预编译,通过 PreparedStatement 和占位符来实现,会把参数部分用一个占位符?
替代,而后注入的参数将不会再进行 SQL 编译,而是当作字符串处理。可以有效避免 SQL 注入漏洞。 -
${}
表示使用拼接字符串,将接受到参数的内容不加任何修饰符拼接在 SQL 中。易导致 SQL 注入漏洞。
两者的区别如下:
-
#{}
为参数占位符?
,即 SQL 预编译。${}
为字符串替换,即 SQL 拼接。 -
#{}
是“动态解析->预编译->执行”的过程。${}
是“动态解析->编译->执行”的过程。 -
#{}
的变量替换是在 DBMS 中。${}
的变量替换是在 DBMS 外。 -
变量替换后,
#{}
对应的变量自动加上引号。变量替换后,${}
对应的变量不会加上引号。
2、漏洞复现
下面以一个查询场景进行简单演示,数据库表 user_table 的表数据如下:
若没有采用 JDBC 的预编译模式,查询 SQL 写为:
<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO">
select * from user_table where username = '${username}'
</select>
这种写法就产生了 SQL 语句的动态拼接,这样格式的参数会直接参与 SQL 语句的编译,从而不能避免SQL注入攻击。
若此时攻击者提交的参数值为 zxd' or 1='1
,如下图,利用 SQL 注入漏洞,成功查询了所有用户数据。
因此,应用 Mybatis 框架 SQL语句的安全写法(即 JDBC 预编译模式):
<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO">
select * from user_table where username = #{username}
</select>
可见,此时采用 JDBC 预编译模式,即使攻击者尝试 SQL 注入攻击,也只会将参数整体作为字符串处理,有效避免了 SQL 注入问题。
5.Mybatis 框架下的 SQL 注入问题及防护方法
还是以上节的查询场景举例,Mybatis 框架下易产生 SQL 注入漏洞的情况主要有以下三种:
1)模糊查询
在模糊查询场景下,考虑安全编码规范,使用 #{}
传入参数:
<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO">
select * from user_table where username like '%#{username}%'
</select>
在这种情况下使用 #{}
程序会报错:
于是很多安全经验不足的程序员就把 #{}
号改成了 ${}
,如果应用层代码没有对用户输入的内容做处理势必会产生SQL注入漏洞。
<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO">
select * from user_table where username like '%${username}%'
</select>
若此时攻击者提交的参数值为 zxd' or 1=1#
,如下图,利用 SQL 注入漏洞,成功查询了所有用户数据。
因此,安全的写法应当使用 CONCAT 函数连接通配符:
<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO">
select * from user_table where username like concat('%',#{username},'%')
</select>
2)带有 IN 谓词的查询
在 IN 关键字之后使用 #{}
查询多个参数:
<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO">
select * from user_table where username in (#{usernames})
</select>
正常提交查询参数 'zxd','hhh'
,因为预编译机制,系统将我们输入的字符当作了一个字符串,因此查询结果为空,不能满足业务功能需求。
于是很多安全经验不足的程序员就把 #{}
号改成了 ${}
:
<select id="getUser" parameterType="java.lang.String" resultType="user.NewUserDO">
select * from user_table where username in (${usernames})
</select>
攻击者提交参数值 'hhh') or 1=1#
,利用 SQL 注入漏洞,成功查询了所有用户数据。
因此,此种情况下,安全的做法应当使用 foreach 标签:
<select id="getUserFromList" resultType="user.NewUserDO">
select * from user_table where username in
<foreach collection="list" item="username" open="(" separator="," close=")">
#{username}
</foreach>
</select>
3)带有动态排序功能的查询
动态排序功能,需要在 ORDER BY 之后传入参数,考虑安全编码规范,使用 #{}
传入参数:
<select id="getUserOrder" parameterType="java.lang.String" resultType="user.NewUserDO">
select * from user_table order by #{column} limit 0,1
</select>
提交参数 username
根据用户名字段排序。但因为预编译机制,系统将我们输入的字符当作了一个字符串,根据字符串排序是不生效的,不能满足业务功能需求。(根据用户名字段排序,此时正常应返回 root
用户)
于是很多安全经验不足的程序员就把 #{}
号改成了 ${}
:
<select id="getUserOrder" parameterType="java.lang.String" resultType="user.NewUserDO">
select * from user_table order by ${column} limit 0,1
</select>
攻击者提交参数值 username#
,利用 SQL 注入漏洞,成功查询了所有用户数据。
因此,此种情况下,安全的做法应当在 Java 代码层面来进行解决。可以设置一个字段值的白名单,仅允许用户传入白名单内的字段。
String sort = request.getParameter("sort");
String[] sortWhiteList = {"id", "username", "password"};
if(!Arrays.asList(sortWhiteList).contains(sort)){
sort = "id";
}
或者仅允许用户传入索引值,代码再将索引值映射成对应字段。
String sort = request.getParameter("sort");
switch(sort){
case "1":
sort = "id";
break;
case "2":
sort = "username";
break;
case "3":
sort = "password";
break;
default:
sort = "id";
break;
}
需要注意的是在 mybatis-generator 自动生成的 SQL 语句中,ORDER BY 使用的也是 ${}
,而 LIKE 和 IN 没有问题。
Mybatis插件
MyBatis 使用 RowBounds 对象进行分页,它是针对 Resultset 结果集执行的内存分页,而非物理分页。可以在sql 内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
分页插件的基本原理是使用 Mybatis 提供的插件接口,实现自定义插件,拦截 Executor 的 query 方法在执行查询的时候,拦截待执行的 sql,然后重写 sql,根据 dialect 方言,添加对应的物理分页语句和物理分页参数。
举例:
select *from student
,拦截sql后重写为:select.*from(select*from student) limit 0,10
Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件。Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,当然,只会拦截那些你指定需要拦截的方法。
实现Mybatis的Interceptor接口并复写intercept()方法,然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可。