up:: SQL注入攻击

接上篇博客,沿用JDBC分析按部门查询员工功能这篇博客中的案例代码;

采用PreparedStatement的策略,能够解决SQL注入攻击的问题。


1.PreparedStatemen简介

PreparedStatement:预编译Statement,其目的是解决SQL注入攻击的问题。

PreparedStatement:

(1) PreparedStatement本质也是一个接口,只是其继承自Statement接口;专用于对SQL语句进行预处理以后再去执行;

(2) 在实际工作中,推荐使用PreparedStatement;


2.PreparedStatement和Statement比较

(1)Statement:

(2)PreparedStatement:

PreparedStatement用法和Statement用法差别还是挺大的;

(1) sql中的?相当于是一个参数,这个参数是需要PreparedStatement进行解析的;PreparedStatement对象提供了set***()方法来设置参数;

(2) 【setString(1,dname)】中的1代表传入第一个参数,值为dname;

(3) PreparedStatement的作用是:【对sql进行预处理】,并且【对字符串中诸如单引号这样的特殊字符进行转义处理】,所以在执行的时候最终的SQL就变成了:

 select * from employee where dname='\' or 1=1 or 1=\''
 

而其中, dname的值就是:【’ or 1=1 or 1=’】(也就是’ or 1=1 or 1=‘这个字符串),即dname是这个整个字符串,(这个字符串不会被作为SQL语句的一部分了,只是作为一个变量的值而已) 所以最终就是查询不到任何结果;从而避免了注入攻击的情况。

(4) 总结一下:即 PreparedStatement根本就是,在SQL执行前进行参数化的处理,通过?将原有需要传入变量的部分,变成在下面通过set***(1,dname)通过参数进行传入的形式,同时对参数中的特殊字符进行转义的处理;


3.PreparedStatement案例

(1) PstmtQueryCommand编写

沿用JDBC分析按部门查询员工功能这篇博客中的案例代码;

PstmtQueryCommand类:

    package com.imooc.jdbc.command;
 
    import java.sql.*;
    import java.util.Scanner;
 
    public class PstmtQueryCommand implements Command {
 
        @Override
        public void execute() {
            System.out.println("请输入部门名称");
            Scanner sc = new Scanner(System.in);
            String pdname = sc.nextLine();
            // 将这三个对象拿到外边,扩大其生命周期;同时这三个对象都内置了close()方法;
            Connection conn = null;
            PreparedStatement pstmt = null;
            ResultSet rs = null;
            try {
                // 1.加载并注册JDBC驱动
                Class.forName("com.mysql.cj.jdbc.Driver");
                // 2.创建数据库连接
                conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/imooc?useSSL=false&useUincode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true", "root", "12345");
                // 3.创建Statement对象
                String sql = "select * from employee where dname=? and eno > ?";//在参数值的地方用一个?替代;在传输数值的地方不需要写单引号啦;
                pstmt = conn.prepareStatement(sql);// 至此,预编译的prepareStatement就设置好了;接下来就是给参数设置值;
                // 这样以后,在运行之前prepareStatement就会将pdname的数据经过敏感字符转义后,放入到上面的?的位置
                pstmt.setString(1,pdname);  // 第一个参数1是参数的索引,第二个参数是参数的值;
                pstmt.setInt(2,3500);
                // 结果集
                rs = pstmt.executeQuery();
                System.out.println("select  * from employee where dname='"+pdname+"'");
                // 4.遍历查询结果
                // next()方法返回一个Boolean值,代表是否存在下一条记录;这个结果集有点迭代器的赶脚;
                // 如果有,返回true,同时结果集提取下一条记录
                // 如果没有,返回false,循环就会停止;
                // 默认,在开始执行下面的循环之前,结果集定位在第一条记录之前;;当循环时,就会尝试获取第一条记录,如果有返回true,
                // 就会把第一条记录提取出来,然后在循环体中进行读取;如果没有直接跳出。
                while (rs.next()) {
                    Integer eno = rs.getInt(1);
                    String ename = rs.getString("ename");
                    Float salary = rs.getFloat("salary");
                    String dname = rs.getString("dname");
                    System.out.println(dname + "-" + eno + "-" + ename + "-" + salary);
                }
 
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (SQLException e) {
                e.printStackTrace();
            }finally {
                // 5.关闭连接,释放资源
                // 这儿演示了对所有资源的完整释放,以后在实际开发中,只写【conn.close();】是没问题的,因为物理连接一旦断开,所有的资源一定会被自动的释放;
                // 这儿之所以三个都写出来了,主要目的是要明白这背后的完全体是什么样子的;
                // 下面三个关闭都可能抛出SQLException,分别使用try块对其进行了处理;
                try {
                    if (rs != null) {  // 如果rs != null,代表rs被实例化了,所以这儿需要关闭一下,进行释放;
                        rs.close();
                    }
                } catch (SQLException e) {
                    e.printStackTrace();
                }
                try {
                    if (pstmt != null) {  // 如果rs != null,代表stmt被实例化了,所以这儿需要关闭一下,进行释放;
                        pstmt.close();
                    }
                } catch (SQLException e) {
                    e.printStackTrace();
                }
                try {
                    if (conn != null && conn.isClosed() == false) {  // conn.isClosed()==false代表这个连接还没有关闭,还正在使用中;
                        conn.close();
                    }
                } catch (SQLException e) {
                    e.printStackTrace();
                }
 
            }
        }
    }
 

PstmtQueryCommand类的几点说明:

(1) 【 pstmt.setString(1,pdname); //第一个参数1是参数的索引,第二个参数是参数的值;】那么以后如果有多个参数的情况如下:和预期相同~~

(2) PrepareStatement对象的有很多set***()方法,按需选用;

(3) 注意关键位置的注释;

(2)运行

为了演示,在入口类【HumanResourceApplication】对象实例化的时候,改成实例化【 PstmtQueryCommand类对象 】:

启动程序,效果如下:


4.为什么使用参数化SQL的方式比拼接字符串的方式更加高效?(即为什么使用PrepareStatement的方式?)

为什么?

(1)如果采用字符串拼接的方式(即Statement的方式):【String sql = “select * from employee where dname=’“+pdname+”’”;】

在实际中,pdname就会被实际的值所替换,即比如查询市场部的时候,sql字符串就是【String sql = “select * from employee where dname=‘市场部’”;】在执行的时候,这条sql字符串是需要被解析才能执行的;

但如果后面又要查询研发部的时候,sql字符串又变成了【String sql = “select * from employee where dname=‘研发部’”;】,这是一条全新的字符串,在执行的时候,这条sql字符串还是需要被解析然后才能执行;

每一次pdname的值不同的时候,都会得到一个新的字符串,因为是新的字符串,所以MySQL每次都要重新解析;

(2)采用参数化字符串的方式(即PrepareStatement的方式):【String sql = “select * from employee where dname=? and eno > ?”;】

将变化的地方都使用了?来指代;即sql字符串是不变的;所以,MySQL只会对sql字符串解析一次,然后将其缓存起来;之后,在具体执行sql的时候,只是将不同的值带入到原始的sql字符串中;

即采用参数字符串的方式时: 即便执行一亿次MySQL的操作,对于sql字符串的文本的解析数量只有1次;

即【字符串拼接的方式】的方式,MySQL解析sql字符串的次数会很大;【参数化字符串的方式】,MySQL解析sql字符串只有1次;虽然解析一次只会花费几毫秒,但是如果高频词高压力的系统下,多次解析带来的额外负担还是很严重的。

所以,在实际开发中应该采用【PrepareStatement】的方式,因为【PrepareStatement】不仅可以解决SQL注入的问题,还能提高程序的执行效率;


5:SQL传参中几种错误的使用方式