浅析预编译防止SQL注入的原理

文章目录

    • 一、什么是SQL注入
    • 二、预编译防止SQL注入
    • 三、mybatis中如何防止SQL注入

一、什么是SQL注入

要理解防止SQL注入的原理,那么首先需要知道什么是SQL注入。百度百科对SQL注入的解释如下:
浅析预编译防止SQL注入的原理_第1张图片
对于理论,这里不作过多赘述,通过一个例子来说明什么是SQL注入。
假如我们有一个登录页面,登录页面大致如下图所示。
浅析预编译防止SQL注入的原理_第2张图片
这个时候,用户正常登陆就是输入用户名和密码进行登录。
我们大致写一个后台的登录逻辑(这里大家不必过于较真,实际项目的登录逻辑肯定与下面代码是有出入的,这里主要是为了对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;
    }

用户表如下:
浅析预编译防止SQL注入的原理_第3张图片
登录时,当输入正确的用户名和密码,比如输入用户名:admin,密码:1234
在主函数中模仿前端输入用户名和密码,调用登录方法。

   public static void main(String[] args) {
        boolean isSuccess = Test2.login("admin", "1234");
        if(isSuccess){
            System.out.println("登录成功,跳转到首页");
        }else {
            System.out.println("用户名或密码不正确,请重新输入");
        }
    }

此时,运行结果如下图:
浅析预编译防止SQL注入的原理_第4张图片

当用户名或者密码输错了,比如输入用户名:admin,密码:12345
执行结果如下:
浅析预编译防止SQL注入的原理_第5张图片
到目前为止,一切看起来都比较合理。但是永远不要相信用户的输入;如果用户输入用户名:任意
密码:’ or 1=‘1
结果如下:
浅析预编译防止SQL注入的原理_第6张图片
为什么会这样呢?
原因就在于**’ or 1='1**

在代码中增加一个打印语句,将拼接后的SQL打印出来
浅析预编译防止SQL注入的原理_第7张图片
密码输入**’ 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("连接数据库失败");
        }
    }

打印的信息如下:
浅析预编译防止SQL注入的原理_第8张图片
查询到的结果与上面我们展示的user表的的数据完全一致,由此可见,确实是查询到了user表中的全部信息。

这个典型的通过SQL注入绕过用户名与密码验证的漏洞!但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("连接失败");
        }
    }
}

浅析预编译防止SQL注入的原理_第9张图片

可以看到,使用了预编译之后,’ or 1='1变得不管用了,最终查询到0条数据,因为数据库里面没有用户名为1234,密码为**’ or 1='1**的数据。

而我们输入正确的用户名和密码时,能够查询到相应的数据。
浅析预编译防止SQL注入的原理_第10张图片

对于预编译语句来说,每个pstmt都与一个sql模板绑定在一起,先把sql模板给数据库,数据库先进行校验,再进行编译。执行时只是把参数传递过去而已!

String sql = "select * from user where username = ? and password = ?";

上述带问号的SQL就是预编译语句的SQL模板,执行前需要先把真正的参数传进去替换掉问号,然后在执行。
浅析预编译防止SQL注入的原理_第11张图片
并且替换的时候,如果是用setString()方法用String类型的参数去替换问号,替换后的结果会自动加上单引号
浅析预编译防止SQL注入的原理_第12张图片
这个地方由于,我们给的参数里面包含单引号,所以还对其添加了转义字符“\”
这样一来的话,就不会存在上面拼接SQL时出现的情况了,从而防止了SQL注入

三、mybatis中如何防止SQL注入

在mybatis中,使用#{ },其底层实际上就是使用了预编译的方式,所以使用#{ }可以防止SQL注入;而${ }实际上就是使用的拼接SQL的方式,这会有SQL注入的风险。

演示一下在mybatis中使用#{ }
浅析预编译防止SQL注入的原理_第13张图片
浅析预编译防止SQL注入的原理_第14张图片
Xml文件中写的#{} 它会给你转成一个?占位符; 传入值替换?的时候 是会加上一个单引号的
所以上面mybatis实际执行的SQL语句如下:
在这里插入图片描述
从上面的过程也可以看出 在mybatis中使用#{ } 传递,与JDBC中预编译方式一致,可以防止SQL注入。

你可能感兴趣的:(sql,数据库,web安全)