• JDBC 学习笔记(六)—— PreparedStatement


    1. 引入 PreparedStatement

    PreparedStatement 通过 Connection.createPreparedStatement(String sql) 方法创建,主要用来反复执行一条结构相似的 SQL 语句。

    例如:

    INSERT INTO STUDENT (STUDENT_NAME, STUDENT_PASSWORD) VALUES ('van Nistelrooy', '666666');
    
    INSERT INTO STUDENT (STUDENT_NAME, STUDENT_PASSWORD) VALUES ('van der Sar', '777777');
    

    这两条 SQL 语句,除了插入的值不同,其他的基本语法没有任何区别。

    针对这种情况,可以使用占位符 ?来代替:

    INSERT INTO STUDENT (STUDENT_NAME, STUDENT_PASSWORD) VALUES (?, ?);

    Statement 是不允许使用 ? 占位符的,而且这个占位符需要获得值之后才能够执行。PreparedStatement 就是针对这种场景才引入进来的。

    PreparedStatement 是 Statement 的子接口,它支持以下4种执行 SQL 的方式:

    • execute()
    • executeUpdate()
    • executeQuery() 
    • executeLargeUpdate() 

    同时,PreparedStatement 提供了一系列的 setXxx(int index, Xxx value) 来支持对于 ? 占位符的替换。Xxx 是占位符的数据类型。

    如果不确定数据类型,可以通过 setObject() 方法来传入数据。

    2. Demo

    这里贴一个自己写的例子:

    package com.gerrard.demo;
    
    import com.gerrard.entity.Student;
    import com.gerrard.util.Connector;
    import com.gerrard.util.DriverLoader;
    
    import java.sql.Connection;
    import java.sql.PreparedStatement;
    import java.sql.SQLException;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    
    public final class PreparedStatementDemo {
    
        public static void main(String[] args) {
            String sql = "INSERT INTO STUDENT (STUDENT_NAME, STUDENT_PASSWORD) VALUES (?, ?)";
    
            Student student1 = new Student(0, "van Nistelrooy", "666666");
            Student student2 = new Student(0, "van der Sar", "777777");
            List<Student> studentList = new ArrayList<>(Arrays.asList(student1, student2));
    
            DriverLoader.loadSqliteDriver();
            try (Connection conn = Connector.getSqlConnection();
                 PreparedStatement pstmt = conn.prepareStatement(sql)) {
    
    
                for (Student student : studentList) {
                    pstmt.setString(1, student.getName());
                    pstmt.setObject(2, student.getPassword());
                    pstmt.executeUpdate();
                }
                System.out.println("Successfully executeUpdate using PreparedStatement.");
    
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
    

    3. PreparedStatement 的优势

    PreparedStatement 的优势主要有以下3点:

    • 通过预编译 SQL 语句的方式(即创建 PreparedStatement 的时候,就将 SQL语句传递进去),大大降低了多次执行相似的 SQL 语句的效率。
    • 需要传递参数的时候,无需拼接 SQL 语句,降低了编程的复杂度。
    • 防止 SQL 注入。

    4. SQL 注入

    SQL 注入是一个常见的 Cracker 入侵方式,利用 SQL 语句的漏洞来入侵。

    那么怎么去理解 PrepareStatement 能够防止 SQL 注入呢?

    实际上,这是不使用 SQL 字符串拼接的副产品。

    现在假设,有一个 GUI 界面,需要输入用户名和密码来登录。

    在后台,我提供了一个登陆服务:

    public interface StudentLoginService {
    
        Student login(String id, String password);
    }
    

    那么现在,我用 Statement 去实现这个服务:(有些自定义的类,详情见 JDBC 学习笔记(十)—— 使用 JDBC 搭建一个简易的 ORM 框架

    package com.gerrard.service;
    
    import com.gerrard.entity.Student;
    import com.gerrard.executor.SqlExecutorStatement;
    import com.gerrard.orm.FlexibleResultSetAdapter;
    import com.gerrard.orm.ResultSetAdapter;
    
    import java.util.List;
    
    public final class StatementLoginService implements StudentLoginService {
    
        private static final String VALIDATE_SQL = "select * from STUDENT s where s.[STUDENT_ID] = ? and s.[STUDENT_PASSWORD] = ?";
    
        @Override
        public Student login(String id, String password) {
            ResultSetAdapter<Student> adapter = new FlexibleResultSetAdapter<>(Student.class);
            SqlExecutorStatement<Student> executor = new SqlExecutorStatement<>(adapter);
            String sql = VALIDATE_SQL.replaceFirst("\?", id).replaceFirst("\?", "'" + password + "'");
            List<Student> list = executor.executeQuery(sql);
            return list.isEmpty() ? null : list.get(0);
        }
    }
    

    这样写代码,看似没有问题,但是实际上有个致命的缺陷。

    假设有人猜测到了这个登陆服务是用 Statement 实现的,那么他在密码一栏可以输入:

    ' or 1=1 or '

    这样,整句 SQL 就变成了:

    select * from STUDENT s where s.[STUDENT_ID] = 1 and s.[STUDENT_PASSWORD] = '' or 1=1 or ''

    显然,所有的 Student 都被查到了,登陆成功。

    但是,使用 PreparedStatement 就没有这种问题,Cracker 输入会被拒绝登陆:

    package com.gerrard.service;
    
    import com.gerrard.entity.Student;
    import com.gerrard.executor.SqlExecutorPreparedStatement;
    import com.gerrard.orm.FlexibleResultSetAdapter;
    import com.gerrard.orm.ResultSetAdapter;
    
    import java.util.Arrays;
    import java.util.LinkedList;
    import java.util.List;
    
    public final class PreparedStatementLoginService implements StudentLoginService {
    
        private static final String VALIDATE_SQL = "select * from STUDENT s where s.[STUDENT_ID] = ? and s.[STUDENT_PASSWORD] = ?";
    
        @Override
        public Student login(String id, String password) {
            List<Object> params = new LinkedList<>(Arrays.asList(id, password));
            ResultSetAdapter<Student> adapter = new FlexibleResultSetAdapter<>(Student.class);
            SqlExecutorPreparedStatement<Student> executor = new SqlExecutorPreparedStatement<>(adapter, params);
            List<Student> list = executor.executeQuery(VALIDATE_SQL);
            return list.isEmpty() ? null : list.get(0);
        }
    }
    

    最后贴一下实验代码和输出:

    package com.gerrard.demo;
    
    import com.gerrard.entity.Student;
    import com.gerrard.service.PreparedStatementLoginService;
    import com.gerrard.service.StatementLoginService;
    import com.gerrard.service.StudentLoginService;
    
    public final class InjectCase {
    
        public static void main(String[] args) {
    
            String id1 = "6";
            String pass1 = "123456";
    
            String id2 = "6";
            String pass2 = "' or 1=1 or '";
    
            // case 1, normal login with Statement
            StudentLoginService service1 = new StatementLoginService();
            Student student1 = service1.login(id1, pass1);
            if (student1 == null) {
                System.out.println("Login failure.");
            } else {
                System.out.println("Student [" + student1.getName() + "] login success.");
            }
    
            // case 2, cracker login with PreparedStatement
            StudentLoginService service2 = new StatementLoginService();
            Student student2 = service2.login(id2, pass2);
            if (student2 == null) {
                System.out.println("Login failure.");
            } else {
                System.out.println("Student [" + student2.getName() + "] login success.");
            }
    
            // case 3, normal login with Statement
            StudentLoginService service3 = new PreparedStatementLoginService();
            Student student3 = service3.login(id1, pass1);
            if (student3 == null) {
                System.out.println("Login failure.");
            } else {
                System.out.println("Student [" + student3.getName() + "] login success.");
            }
    
            // case 4, cracker login with PreparedStatement
            StudentLoginService service4 = new PreparedStatementLoginService();
            Student student4 = service4.login(id2, pass2);
            if (student4 == null) {
                System.out.println("Login failure.");
            } else {
                System.out.println("Student [" + student4.getName() + "] login success.");
            }
        }
    }
    

  • 相关阅读:
    C# 日期帮助类【原创】
    C# 发送邮件
    每日一题力扣453
    每日力扣628
    每日一题力扣41巨坑
    每日一题力扣274
    每日一题力扣442有坑
    每日一题力扣495
    每日一题力扣645
    每日一题力扣697
  • 原文地址:https://www.cnblogs.com/jing-an-feng-shao/p/9221158.html
Copyright © 2020-2023  润新知