详解MyBatis篇四
目录
#{}和${}
#{}和${}的使用
针对Interger类型
针对String类型
改正代码
小结
#{} 和 ${} 的区别
#{}的优点
性能更高
更安全(防止SQL注入)
SQL注入场景
正常登录
SQL注入
${}的应用场景示例
排序功能
like查询
总结
#{}和${}
#{}和${}的使用
针对Interger类型
mapper接口
@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where id = #{id}\") UserInfo getUserInfo1(@Param(\"id\") Integer id);}
Test代码
@SpringBootTestclass UserInfoMapperTest { @Autowired private UserInfoMapper userInfoMapper; @Test void getUserInfo1() { System.out.println(userInfoMapper.getUserInfo1(3)); }}
运行结果:
我们输⼊的参数并没有在后⾯拼接,id的值是使⽤ ? 进⾏占位. 这种SQL 我们称之为\"预编译SQL\"。
mapper接口
@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where id = ${id}\") UserInfo getUserInfo1(@Param(\"id\") Integer id);}
Test代码
@SpringBootTestclass UserInfoMapperTest { @Autowired private UserInfoMapper userInfoMapper; @Test void getUserInfo1() { System.out.println(userInfoMapper.getUserInfo1(3)); }}
运行结果:
可以看到, 这次的参数是直接拼接在SQL语句中了.
针对使用#{}和${}的对比
针对String类型
mapper接口
@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where username = #{username}\") UserInfo getUserInfo2(@Param(\"username\") String username);}
Test代码
@SpringBootTestclass UserInfoMapperTest { @Autowired private UserInfoMapper userInfoMapper; @Test void getUserInfo2() { System.out.println(userInfoMapper.getUserInfo2(\"xiaoming\")); }}
运行结果:
mapper接口
@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where username = ${username}\") UserInfo getUserInfo2(@Param(\"username\") String username);}
Test代码
@SpringBootTestclass UserInfoMapperTest { @Autowired private UserInfoMapper userInfoMapper; @Test void getUserInfo2() { System.out.println(userInfoMapper.getUserInfo2(\"xiaoming\")); }}
运行结果:
可以看到, 这次的参数依然是直接拼接在SQL语句中了, 但是字符串作为参数时, 需要添加引号 \' \' , 使⽤ ${} 不会拼接引号 \' \' , 导致程序报错.
改正代码
mapper接口
@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where username = \'${username}\'\") UserInfo getUserInfo2(@Param(\"username\") String username);}
运行结果:
小结
从上⾯两个例⼦可以看出:
#{} 使⽤的是预编译SQL, 通过 ? 占位的⽅式, 提前对SQL进⾏编译, 然后把参数填充到SQL语句中. #{} 会根据参数类型, ⾃动拼接引号 \'\' .
${} 会直接进⾏字符替换, ⼀起对SQL进⾏编译. 如果参数为字符串, 需要加上引号 \'\' .
参数为数字类型时, 也可以加上, 查询结果不变, 但是可能会导致索引失效, 性能下降.
mapper接口
@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where id = \'${id}\'\") UserInfo getUserInfo1(@Param(\"id\") Integer id);}
运行结果:
#{} 和 ${} 的区别
#{} 和 ${} 的区别就是预编译SQL和即时SQL 的区别.
简单回顾:
当客⼾发送⼀条SQL语句给服务器后, ⼤致流程如下:
1. 解析语法和语义, 校验SQL语句是否正确
2. 优化SQL语句, 制定执⾏计划
3. 执⾏并返回结果
⼀条 SQL如果⾛上述流程处理, 我们称之为 Immediate Statements(即时 SQL)。
#{}的优点
性能更高
绝⼤多数情况下, 某⼀条 SQL 语句可能会被反复调⽤执⾏, 或者每次执⾏的时候只有个别的值不同(⽐如 select 的 where ⼦句值不同, update 的 set ⼦句值不同, insert 的 values 值不同). 如果每次都需要经过上⾯的语法解析, SQL优化、SQL编译等,则效率就明显不⾏了.
预编译SQL,编译⼀次之后会将编译后的SQL语句缓存起来,后⾯再次执⾏这条语句时,不会再次编译(只是输⼊的参数不同), 省去了解析优化等过程, 以此来提⾼效率。
更安全(防止SQL注入)
SQL注⼊:是通过操作输⼊的数据来修改事先定义好的SQL语句,以达到执⾏代码对服务器进⾏攻击的⽅法。
由于没有对⽤⼾输⼊进⾏充分检查,⽽SQL⼜是拼接⽽成,在⽤⼾输⼊参数时,在参数中添加⼀些
SQL关键字,达到改变SQL运⾏结果的⽬的,也可以完成恶意攻击。
SQL注入演示
使用${}
正常代码
mapper接口
@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where username = \'${username}\'\") UserInfo getUserInfo2(@Param(\"username\") String username);}
xml实现
@SpringBootTestclass UserInfoMapperTest { @Autowired private UserInfoMapper userInfoMapper; @Test void getUserInfo2() { System.out.println(userInfoMapper.getUserInfo2(\"xiaoming\")); }}
运行结果:
SQL注入代码
@SpringBootTestclass UserInfoMapperTest { @Autowired private UserInfoMapper userInfoMapper; @Test void getUserInfo2() { System.out.println(userInfoMapper.getUserInfo2(\"\' or 1=\'1\")); }}
运行结果:
使用#{}
mapper接口
@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where username = #{username}\") UserInfo getUserInfo2(@Param(\"username\") String username);}
xml实现
@SpringBootTestclass UserInfoMapperTest { @Autowired private UserInfoMapper userInfoMapper; @Test void getUserInfo2() { System.out.println(userInfoMapper.getUserInfo2(\"xiaoming\")); }}
运行结果:
xml实现
@SpringBootTestclass UserInfoMapperTest { @Autowired private UserInfoMapper userInfoMapper; @Test void getUserInfo2() { System.out.println(userInfoMapper.getUserInfo2(\"\' or 1=\'1\")); }}
运行结果:
SQL注入场景
SQL注⼊是⼀种⾮常常⻅的数据库攻击⼿段, SQL注⼊漏洞也是⽹络世界中最普遍的漏洞之⼀.
如果发⽣在⽤⼾登录的场景中, 密码输⼊为 \' or 1=\'1 , 就可能完成登录(不是⼀定会发⽣的场景,需要看登录代码怎么写)。
mapper接口
@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where username= \'${username}\' and password = \'${password}\'\") List queryUserByNameAndPassword(String username, String password);}
UserService
@Servicepublic class UserService { @Autowired private UserInfoMapper userInfoMapper; public List queryUserByNameAndPassword(String name, String password) { return userInfoMapper.queryUserByNameAndPassword(name, password); }}
UserController
@RequestMapping(\"/user\")@RestControllerpublic class UserController { @Autowired private UserService userService; @RequestMapping(\"/login\") public Boolean login(String name, String password){ //根据账号和密码查询数据库 List userInfo = userService.queryUserByNameAndPassword(name, password); if(userInfo==null){ return false; } return true; }}
数据库信息
正常登录
SQL注入
${}的应用场景示例
排序功能
mapper接口
@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo order by id ${order}\") List queryUserListByOrder(String order);}
Test代码
@SpringBootTestclass UserInfoMapperTest { @Autowired private UserInfoMapper userInfoMapper; @Test void queryUserListByOrder() { System.out.println(userInfoMapper.queryUserListByOrder(\"desc\")); }}
运行结果:
将${}替换成#{}
mapper接口
@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo order by id #{order}\") List queryUserListByOrder(String order);}
运行结果:
不难发现,#{}是给 desc 添加了单引号,但是这句SQL语句的 desc并不应该加单引号。
除此之外, 还有表名作为参数时, 也只能使⽤ ${}.
like查询
mapper接口
@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where username like \'%#{username}%\'\") List queryUserListByLike(String username);}
Test代码
@SpringBootTestclass UserInfoMapperTest { @Autowired private UserInfoMapper userInfoMapper; @Test void queryUserListByLike() { System.out.println(userInfoMapper.queryUserListByLike(\"zh\")); }}
运行结果:
将#{}改为${}
@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where username like \'%${username}%\'\") List queryUserListByLike(String username);}
运行结果:
将#{}改为${}后,虽然能正确查出来,但是存在SQL注入问题,因此不能直接用${}。
解决方法:使用MySQL的内置函数concat来处理:
mapper代码
@Mapperpublic interface UserInfoMapper { @Select(\"select * from userinfo where username like CONCAT(\'%\',#{username},\'%\')\") List queryUserListByLike(String username);}
运行结果:
总结
1.预编译处理:
#{} 是预编译(PreparedStatement)的占位符。当使用#{}时,MyBatis 会将 SQL 中的#{}替换为?,然后通过 PreparedStatement 的 setXXX 方法来设置参数值。这样做的好处是可以防止 SQL 注入,因为 PreparedStatement 会将传入的参数值当做字符串处理,并对其进行转义。
${} 是字符串替换的占位符。MyBatis 在处理 SQL 时,会直接将${}中的变量替换为变量的值,然后再进行 SQL 的编译。这意味着如果变量来自于用户输入,并且没有进行适当的处理,那么就有可能出现 SQL 注入的风险。
2.使用场景:
#{} 主要用于 SQL 语句中的值替换,特别是当需要防止 SQL 注入时。它是 MyBatis 推荐的参数替换方式。
${} 主要用于动态 SQL 的构建,比如表名、列名、动态 SQL 片段等。因为这些情况下,#{}无法做到字符串的直接替换,而${}则可以实现。但是,由于${}存在 SQL 注入的风险,所以在使用时需要格外小心,确保替换的变量是安全的。
3.参数类型:
#{} 可以接收简单类型、POJO(Plain Old Java Object)类型、Map 类型等,并且 MyBatis 会自动处理这些类型的参数,将其设置到 SQL 语句中相应的位置。
${} 主要用于字符串替换,所以它接收的主要是字符串类型的参数。当然,你也可以传递其他类型的参数,但 MyBatis 会将其转换为字符串形式进行替换。