区别1:
当有@Parm("xx")指明参数名称时,对于数值型数据,#和$不存在实质性的区别,对于字符型数据,$取出的值是不会自带引号''的。
数值型实验:
1. 在DAO中添加方法
User queryUserById(@Param("id") Integer id);
2. 编写Mapper.xml
A. 采用#取值
<sql id="user_field">
select id,username,password,gender,regist_time as registerTime
from t_user
</sql>
<select id="queryUserById" resultType="User">
<include refid="user_field"></include>
where id = #{id}
</select>
B. 采用$取值
<sql id="user_field">
select id,username,password,gender,regist_time as registerTime
from t_user
</sql>
<select id="queryUserById" resultType="User">
<include refid="user_field"></include>
where id = concat("'",${id},"'")
</select>
[注意!!!]:通过控制台输出的SQL语句我们也可以发现:
这是#输出的SQL;
这是$输出的SQL;
$做的是直接的字符串拼接,而#显然不是,是采用占位符的方式,它会根据是否是字符型选择是否自行添加' '。
字符型实验:
1. 在DAO中添加方法
User queryUserByUsername(@Param("username") String username);
2. 编写Mapper.xml
A. 采用#取值
<select id="queryUserByUsername" resultType="User">
<include refid="user_field"></include>
where username = #{username}
</select>
B. 采用$取值
<select id="queryUserByUsername" resultType="User">
<include refid="user_field"></include>
where username = ${username}
</select>
正确写法:
<select id="queryUserByUsername" resultType="User">
<include refid="user_field"></include>
where username = '${username}'
</select>
区别2:
在没有使用@Param注解时,#支持零散参数用arg0、arg1或者param1、param2,但是$不支持。使用@Param时,#和$都支持param。
A. @Param注解 + #{param1}
<select id="queryUserById" resultType="User">
<include refid="user_field"></include>
where id = #{param1}
</select>
B. @Param注解 + ${param}
<select id="queryUserById" resultType="User">
<include refid="user_field"></include>
where id = ${param1}
</select>
区别依旧是SQL语句的不同,如果读者还有印象的话,PreparedStatement主要的作用就是防止SQL注入,后面会说。
需要注意:如果参数使用了@Param注解,则#只支持param而不支持arg。
C. 去除@Param注解
1)#{arg0}或#{param1}
User queryUserById(Integer id);
<select id="queryUserById" resultType="User">
<include refid="user_field"></include>
where id = #{arg0}
</select>
<select id="queryUserById" resultType="User">
<include refid="user_field"></include>
where id = #{param1}
</select>
2)${arg0}或${param1}
<select id="queryUserById" resultType="User">
<include refid="user_field"></include>
where id = ${arg0}
</select>
由报错信息我们可以看到,去除@Param注解后,${arg0}实际上将arg0当作是类参数Integer id的一个名叫arg0的属性,企图通过get方法取值。
<select id="queryUserById" resultType="User">
<include refid="user_field"></include>
where id = ${param1}
</select>
由报错信息我们可以看到,去除@Param注解后,${param1}实际上将param1当作是类参数Integer id的一个名叫param1的属性,企图通过get方法取值。
值得注意:在Mybatis中,即便我们在DAO接口中设置的方法的参数是基本数据类型,在Mybatis生成时依旧会将参数变为基本数据类型的包装类,如:
User queryUserById(int id);
<select id="queryUserById" resultType="User">
<include refid="user_field"></include>
where id = ${id}
</select>
[总结]:如果一定要使用$,那么对于零散的参数,一定要加注解。对于字符型或字符串,需要手动添加引号。
区别3[重要]:
SQL注入指的是我们可以通过SQL语句,绕开仅仅使用SQL语句做的安全检查。例如我们要登陆系统,一般都是验证用户名或密码。
我们要执行的正常SQL语句:"select * from t_user where username = ' " + username + " ' and password = ' " + password + " ' "。
但是如果我们将password赋值为'123456 or 1 = 1',那么执行的SQL语句会变成:
select * from t_user where username = ' " + username + " ' and 123456 or 1 = 1 ' ",由于1=1是成立条件,因此这条语句可以将表里的数据全部查出,这就会完全绕开用SQL做的安全检查。
我们做一个示例:
List<User> Login(@Param("username") String id,@Param("password") String password);
<select id="Login" resultType="User">
select * from t_user
where username = '${username}' and password = '${password}'
</select>
@Test
public void testLogin(){
List<User> zzt = mapper.Login("zzt", "111' or '1' = '1");
for (User user : zzt) {
System.out.println(zzt);
}
}
细心的读者可能也发现了,我们这样做的前提是整个方法的返回必须是个List,如果是实体类的类型,则会因为要封装的对象太多导致错误。
但是这并不代表着我们就不需要担心SQL注入的问题,如果整个系统的登录逻辑是,只要查出来的系统的数据中存在超过一条,就可以放行,特别是对于一些账号和密码允许重复,即用其他值作为主键的情况。
另外,我们来谈一种更可能的情况,我们知道对于数据库的大量查询操作是相当费时间的,通过SQL注入我们可以查询所有的数据,那么只要有客户端不断向服务器端进行SQL注入,服务器可能会因为大量的查询导致缓存耗尽、影响系统响应甚至宕机的情况。
那么#使用占位符为何能避免SQL注入的问题呢?我们看一眼SQL语句便知:
实际上翻译过来就是:select * from t_user where username = 'zzt' and password = "111' or '1' = '1 ", 也就是说,111' or '1' = '1被当作了密码,自然就防止了注入
但是!但是!但是!在某些情况下,也是不能使用占位符的,例如让数据按照动态的规则进行有序查询输出:select * from t_user order by id desc;
List<User> queryByDesc(@Param("order") String order);
<select id="queryByDesc" resultType="User">
select * from t_user order by id #{order}
</select>
@Test
public void testQueryByDesc(){
List<User> users = mapper.queryByDesc("desc");
for (User user : users) {
System.out.println(user);
}
}
显然:select * from t_user order by id 'desc';是有语法错误的。
使用拼接的方式:
<select id="queryByDesc" resultType="User">
select * from t_user order by id ${order}
</select>
为了保证安全性,防止注入,我们不允许用户直接输入规则,只允许输入规则对应的参数:
@Test
public void testQueryByDesc(){
int signal = 0;
List<User> users;
if( signal == 0 ){
users = mapper.queryByDesc("desc");
}else{
users = mapper.queryByDesc("asc");
}
for (User user : users) {
System.out.println(user);
}
}
[总结]:在要填充的数据与列属性相关时,用占位符确实可以规避SQL注入的风险,其他情况(与SQL本身有关)使用,则极有可能导致SQL语法错误。SQL拼接会有注入风险,但是只要语法保证正确就没有限制(此处限制指的是可以使用而不是防止注入导致的安全性等问题)。大部分情况下我们还是推荐使用#。