要理解防止SQL注入的原理,那么首先需要知道什么是SQL注入。百度百科对SQL注入的解释如下:
对于理论,这里不作过多赘述,通过一个例子来说明什么是SQL注入。
假如我们有一个登录页面,登录页面大致如下图所示。
这个时候,用户正常登陆就是输入用户名和密码进行登录。
我们大致写一个后台的登录逻辑(这里大家不必过于较真,实际项目的登录逻辑肯定与下面代码是有出入的,这里主要是为了对SQL注入进行一个简单的演示)。
登录逻辑:
public static boolean login(String username,String password) {
// 数据库连接信息
String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/test";
String uid = "root";
String pwd = "9527";
try {
// 加载驱动
Class.forName(driver);
// 建立数据库连接
Connection conn = DriverManager.getConnection(url, uid, pwd);
// 拼接SQL语句
String sql = "select * from user where username =" + "'" + username + "'" + "and password =" + "'" + password + "'";
Statement statement = conn.createStatement();
// 执行查询SQL
ResultSet resultSet = statement.executeQuery(sql);
if (resultSet.next()){
System.out.println("有符合条件的用户");
return true;
}else {
System.out.println("没有符合条件的用户");
return false;
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("连接数据库失败");
}
return false;
}
用户表如下:
登录时,当输入正确的用户名和密码,比如输入用户名:admin,密码:1234
在主函数中模仿前端输入用户名和密码,调用登录方法。
public static void main(String[] args) {
boolean isSuccess = Test2.login("admin", "1234");
if(isSuccess){
System.out.println("登录成功,跳转到首页");
}else {
System.out.println("用户名或密码不正确,请重新输入");
}
}
当用户名或者密码输错了,比如输入用户名:admin,密码:12345
执行结果如下:
到目前为止,一切看起来都比较合理。但是永远不要相信用户的输入;如果用户输入用户名:任意
密码:’ or 1=‘1
结果如下:
为什么会这样呢?
原因就在于**’ or 1='1**
在代码中增加一个打印语句,将拼接后的SQL打印出来
密码输入**’ or 1='1**之后,经过拼接的SQL语句实际上为
select * from user where username ='1234'and password ='' or 1='1'
上述SQL又等价于
select * from user
因为有了 or 1=1 这条恒成立的条件语句,所以一定能够查到数据库里面的信息,并且能够查到数据库里面的所有用户的信息。
为了验证是否确实查到了user表中的所有用户信息,我们改造了刚刚的login方法,如下:
public static void login2(String username, String password) {
// 数据库连接信息
String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/test";
String uid = "root";
String pwd = "9527";
try {
// 加载驱动
Class.forName(driver);
// 建立数据库连接
Connection conn = DriverManager.getConnection(url, uid, pwd);
// 拼接SQL语句
String sql = "select * from user where username =" + "'" + username + "'" + "and password =" + "'" + password + "'";
System.out.println(sql);
Statement statement = conn.createStatement();
// 执行查询SQL
ResultSet resultSet = statement.executeQuery(sql);
while (resultSet.next()){
System.out.println("有符合条件的用户");
System.out.println("通过注入获取到的字段1=" + resultSet.getString(1));
System.out.println("通过注入获取到的字段2=" + resultSet.getString(2));
System.out.println("通过注入获取到的字段3=" + resultSet.getString(3));
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("连接数据库失败");
}
}
打印的信息如下:
查询到的结果与上面我们展示的user表的的数据完全一致,由此可见,确实是查询到了user表中的全部信息。
这个典型的通过SQL注入绕过用户名与密码验证的漏洞!但SQL注入的风险,远远不止在登录存在,在其他各种输入框的地方都可能会存在SQL注入的风险。而SQL注入可能会带来很大的安全隐患,所以防止SQL注入是非常有必要的。
上面的登录代码,虽然不是很符合真正开发中的登录逻辑,但是也能够很好的帮助我们理解SQL注入了。那么上面的代码为什么会存在SQL注入的漏洞呢?因为用户在输入时,输入了特殊的字符**’ or 1='1**,以至于,在拼接SQL的时候,出现了问题,拼接出一条能够查询出整个表的数据的SQL语句,这是非常不安全的。
那么有没有什么方法能够解决这个问题呢?这要讲到今天的主题了。
我们前面用拼接的方式,可以让人有机可乘,通过特殊代码进行SQL注入;那如果我们不用拼接的方式,而用预编译的方式会有什么不一样呢?为什么预编译的方式可以防止SQL注入呢?
我们同样通过一个小例子来演示:
public class Test {
public static void main(String[] args) {
Test.login("1234", "' or 1='1");
}
public static void login(String username,String password){
String driver = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/test";
String uid = "root";
String pwd = "9527";
try {
Class.forName(driver);
Connection conn = DriverManager.getConnection(url, uid, pwd);
String sql = "select * from user where username = ? and password = ?";
PreparedStatement preparedStatement = conn.prepareStatement(sql);
preparedStatement.setString(1,username);
preparedStatement.setString(2,password);
System.out.println(preparedStatement.toString());
ResultSet resultSet = preparedStatement.executeQuery();
while (resultSet.next()){
System.out.println("uid="+resultSet.getString("uid"));
System.out.println("username="+resultSet.getString("username"));
System.out.println("password="+resultSet.getString("password"));
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("连接失败");
}
}
}
可以看到,使用了预编译之后,’ or 1='1变得不管用了,最终查询到0条数据,因为数据库里面没有用户名为1234,密码为**’ or 1='1**的数据。
对于预编译语句来说,每个pstmt都与一个sql模板绑定在一起,先把sql模板给数据库,数据库先进行校验,再进行编译。执行时只是把参数传递过去而已!
String sql = "select * from user where username = ? and password = ?";
上述带问号的SQL就是预编译语句的SQL模板,执行前需要先把真正的参数传进去替换掉问号,然后在执行。
并且替换的时候,如果是用setString()方法用String类型的参数去替换问号,替换后的结果会自动加上单引号
这个地方由于,我们给的参数里面包含单引号,所以还对其添加了转义字符“\”
这样一来的话,就不会存在上面拼接SQL时出现的情况了,从而防止了SQL注入
在mybatis中,使用#{ },其底层实际上就是使用了预编译的方式,所以使用#{ }可以防止SQL注入;而${ }实际上就是使用的拼接SQL的方式,这会有SQL注入的风险。
演示一下在mybatis中使用#{ }
Xml文件中写的#{} 它会给你转成一个?占位符; 传入值替换?的时候 是会加上一个单引号的
所以上面mybatis实际执行的SQL语句如下:
从上面的过程也可以看出 在mybatis中使用#{ } 传递,与JDBC中预编译方式一致,可以防止SQL注入。