MySQL学习笔记(5)--SQL注入问题及解决方法

模仿客户登录

public class LoginTest1 {
    public static void main(String[] args) {

        // 1. 接收用户数据   [网页文本框中输入 (字符串)]
        Scanner sc = new Scanner(System.in);
        System.out.println("亲, 请输入您的用户名:");
        String username = sc.nextLine();
        System.out.println("亲, 请输入您的密码 :");
        String password = sc.nextLine();
        sc.close();

        // 定义需要释放资源的对象
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;

        try {
            // 2. 建立连接
            conn = JDBCUtils.getConnection();
            // 3. 操作数据
            // 参数拼接 :
            String sql = "select * from user where username = '"+username+"' and password = '"+password+"';";
            stmt = conn.createStatement();      
            rs = stmt.executeQuery(sql);
            // 说明 : 登录中, 如果获取结果集, 有两种可能性. 要么有, 要么没有, 有的话, 就仅有一条数据, 不可能出现多条.
            if (rs.next()) {
                // 取出数据.
                System.out.println("登录成功, 欢迎您." + rs.getString("email"));
            } else {
                System.out.println("亲, 您的用户名或密码错误, 登录失败!");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 4. 释放资源
            JDBCUtils.release(conn, stmt, rs);
        }
    }
}

运行程序,输入如下代码

图1:

这里写图片描述


图2:

这里写图片描述

我们发现,按照上面的方法进行操作,无论密码是否正确,都提示我们登陆成功,这显然是不合理的。问题出在哪里呢?

stmt = conn.createStatement();
rs = stmt.executeQuery(sql);

前面的学习中提到过,执行对象有两个作用,
作用1:将Java字符串转换成SQL语句
作用2:将SQL语句交给MySQL底层进行处理

问题就出现在作用1上:


图1中字符串拼接后的SQL语句是:

select * from user where username = 'zhangsan'-- and password = '"+password+"';

而 – 在MySQL中是注释,运行的时候不考虑,所以实际执行的语句是

select * from user where username = 'zhangsan'

在MySQL底层运行的时候判断条件里面并没有对密码进行验证,所以才会出现输入的密码是什么都提示我们登陆成功

图2中字符串拼接后的SQL语句是:

select * from user where username = 'zhangsan' or 1 = 1 and password = '"+password+"';

运行到or的时候已经是条件成立,所以无论后面是否正确,无需验证密码即可登陆成功。

上面的问题都是通过在SQL语句中添加特殊的字符,构成关键字,改变了程序运行轨迹,从而对数据进行操作。

SQL注入问题:是指通过客户输入到后台的那些能到数据库得到数据的位置上,恶性的输入一些对数据有害的操作。

那么我们应该如何防止此类现现象的产生呢?
可以考虑以下思路
步骤一 : 将未拼接的 Java 字符串转换为 SQL 指令. 这条 SQL 指令是不能执行的, 因为缺少参数.
中间环节 : 将缺少的参数设置为对应的位置.
步骤二 : 直接将指令交给 MySQL 底层执行, 并接收返回结果.

但是我们发现,这个操作我们无法实现,因为方法是底层的,我们无法修改。针对这种情况,Java提供了其他的方法让我们实现步骤,即先进行预编译,然后再进行拼接和执行。这个方法叫preparedExecute()。

代码实现:

模拟用户登录

public class LoginTest2 {
    public static void main(String[] args) {

        // 1. 接收用户数据   [网页文本框中输入 (字符串)]
        Scanner sc = new Scanner(System.in);
        System.out.println("亲, 请输入您的用户名:");
        String username = sc.nextLine();
        System.out.println("亲, 请输入您的密码 :");
        String password = sc.nextLine();
        sc.close();

        // 定义需要释放资源的对象
        Connection conn = null;
        PreparedStatement stmt = null;
        ResultSet rs = null;

        try {
            // 2. 建立连接
            conn = JDBCUtils.getConnection();
            // 3. 操作数据
            // ? 表示占位符.
            // 步骤一 : 将未拼接的 Java 字符串转换为 SQL 指令. 这条 SQL 指令是不能执行的, 因为缺少参数.
            String sql = "select * from user where username = ? and password = ?;";
            stmt = conn.prepareStatement(sql);   预编译

            // 中间环节 : 将缺少的参数设置到对应的位置.
            stmt.setString(1, username);
            stmt.setString(2, password);

            // 步骤二 : 直接将指令交给 MySQL 底层执行, 并接收返回结果.
            rs = stmt.executeQuery();

            // 说明 : 登录中, 如果获取结果集, 有两种可能性. 要么有, 要么没有, 有的话, 就仅有一条数据, 不可能出现多条.
            if (rs.next()) {
                // 取出数据.
                System.out.println("登录成功, 欢迎您." + rs.getString("email"));
            } else {
                System.out.println("亲, 您的用户名或密码错误, 登录失败!");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 4. 释放资源
            JDBCUtils.release(conn, stmt, rs);
        }
    }
}

测试1:增加

@Test
public void insertTest() {

    Connection conn = null;
    PreparedStatement stmt = null;

    try {
        // 1. 建立连接
        conn = JDBCUtils.getConnection();

        // 2. 操作数据
        String sql = "insert into user values(?,?,?,?);";
        // 步骤一 : 预编译 Java字符串为 SQL 指令, 但是指令不能执行.
        stmt = conn.prepareStatement(sql);

        // 中间环节 :
        // stmt.setObject(1, null);   // null != "null"
        stmt.setString(1, null);
        stmt.setString(2, "tianqi");
        stmt.setString(3, "666");
        stmt.setString(4, "[email protected]");

        // 步骤二 : 直接将 SQL 执行交给数据库底层执行
        int count = stmt.executeUpdate();
        System.out.println("count = " + count);

    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        // 3. 释放资源
        JDBCUtils.release(conn, stmt);
    }
}

测试2: 修改

@Test
public void updateTest() {

    Connection conn = null;
    PreparedStatement stmt = null;

    try {
        // 1. 建立连接
        conn = JDBCUtils.getConnection();
        // 2. 操作数据
        String sql = "update user set username = ?, password = ?, email = ? where id = ?;";
        // 步骤一 : 预编译, 将 Java 字符串编译为 SQL 指令
        stmt = conn.prepareStatement(sql);

        // 中间环节 :
        stmt.setString(1, "张三");
        stmt.setString(2, "888");
        stmt.setString(3, "[email protected]");
        stmt.setInt(4, 1);

        // 步骤二 : 直接执行编译完成后的 SQL 执行.
        int count = stmt.executeUpdate();
        System.out.println("count = " + count);
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        // 3. 释放资源
        JDBCUtils.release(conn, stmt);
    }
}

测试3:删除

@Test
public void deleteTest() {

    Connection conn = null;
    PreparedStatement stmt = null;

    try {
        // 1. 建立连接
        conn = JDBCUtils.getConnection();
        // 2. 操作数据
        String sql = "delete from user where id = ?;";
        // 步骤一 : 实现预编译
        stmt = conn.prepareStatement(sql);

        // 中间环节 : 设置参数
        stmt.setInt(1, 7);

        // 步骤二 : 直接执行 sql 指令
        int count = stmt.executeUpdate();
        System.out.println("count = " + count);

    } catch (SQLException e) {

    } finally {
        // 3. 释放资源
        JDBCUtils.release(conn, stmt);
    }
}

回顾前面的学习,我们首先进行了JDBC初体验,了解了原理和一些基本操作;然后为了释放资源,我们使用try…catch…finally方法处理异常,保证即使发生异常的时候资源(尤其是连接资源)能够正常释放;为了提高代码的复用性,我们使用工具类包装四个基本步骤中的三个:加载驱动、获取连接、释放资源;然后我们又学习了利用配置文件给工具类中的属性赋值的方法提高了代码的通用性;最后使用预编译方法,解决了sql注入问题,提高了安全性。

模拟用户登录实例代码:


1.配置文件

driverClass = com.mysql.cj.jdbc.Driver
url = jdbc:mysql://localhost:3306/mydb?serverTimezone=UTC&characterEncoding=UTF-8
username = root
password = 123456

2.JDBC工具类

public class JDBCUtils {
    // 属性 (数据)
    // 静态属性如果使用 final 修饰, 还可以在 `静态代码块中` 实现赋值
    private static final String driverClass;
    private static final String url;
    private static final String username;
    private static final String password;

    // 静态代码块 :
    static {
        // 1. 创建一个 Properties 对象.
        Properties prop = new Properties();

        try {
            // 2. 加载配置文件数据到 prop 对象中
            prop.load(new FileReader("jdbc.properties"));

            // 3. 如果加载成功, 为静态属性赋值
            driverClass = prop.getProperty("driverClass");
            url = prop.getProperty("url");
            username = prop.getProperty("username");
            password = prop.getProperty("password");

            // 加载驱动
            loadDriver();

        } catch (IOException e) {
            // 注意 : 配置文件如果加载失败, 直接抛出一个运行时异常
            throw new RuntimeException("配置文件加载失败!");
        }
    }

    // 1. 加载驱动
    private static void loadDriver() {
        try {
            Class.forName(driverClass);
        } catch (ClassNotFoundException e) {
            // 说明 : 如果驱动加载失败, 之后的所有操作就无需继续执行了...
            throw new RuntimeException("驱动加载失败!");
        }
    }

    // 2. 获取连接
    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(url, username, password);
    }

    // 3. 释放资源
    public static void release(Connection conn, Statement stmt, ResultSet rs) {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            rs = null;  // clear to let GC do its work
        }

        // 调用下面的方法实现 conn, stmt
        release(conn, stmt);
    }

    public static void release(Connection conn, Statement stmt) {

        if (stmt != null) {
            try {
                stmt.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            stmt = null;
        }

        if (conn != null) {
            try {
                conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            conn = null;
        }
    }
}

3.测试类

测试1:增加数据

@Test
public void insertTest() {

    Connection conn = null;
    PreparedStatement stmt = null;

    try {
        // 1. 建立连接
        conn = JDBCUtils.getConnection();

        // 2. 操作数据
        String sql = "insert into user values(?,?,?,?);";
        // 步骤一 : 预编译 Java字符串为 SQL 指令, 但是指令不能执行.
        stmt = conn.prepareStatement(sql);

        // 中间环节 :
        // stmt.setObject(1, null);   // null != "null"
        stmt.setString(1, null);
        stmt.setString(2, "tianqi");
        stmt.setString(3, "666");
        stmt.setString(4, "[email protected]");

        // 步骤二 : 直接将 SQL 执行交给数据库底层执行
        int count = stmt.executeUpdate();
        System.out.println("count = " + count);

    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        // 3. 释放资源
        JDBCUtils.release(conn, stmt);
    }
}

测试2: 修改数据

@Test
public void updateTest() {

    Connection conn = null;
    PreparedStatement stmt = null;

    try {
        // 1. 建立连接
        conn = JDBCUtils.getConnection();
        // 2. 操作数据
        String sql = "update user set username = ?, password = ?, email = ? where id = ?;";
        // 步骤一 : 预编译, 将 Java 字符串编译为 SQL 指令
        stmt = conn.prepareStatement(sql);

        // 中间环节 :
        stmt.setString(1, "张三");
        stmt.setString(2, "888");
        stmt.setString(3, "[email protected]");
        stmt.setInt(4, 1);

        // 步骤二 : 直接执行编译完成后的 SQL 执行.
        int count = stmt.executeUpdate();
        System.out.println("count = " + count);
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        // 3. 释放资源
        JDBCUtils.release(conn, stmt);
    }
}

测试3:删除数据

@Test
public void deleteTest() {

    Connection conn = null;
    PreparedStatement stmt = null;

    try {
        // 1. 建立连接
        conn = JDBCUtils.getConnection();
        // 2. 操作数据
        String sql = "delete from user where id = ?;";
        // 步骤一 : 实现预编译
        stmt = conn.prepareStatement(sql);

        // 中间环节 : 设置参数
        stmt.setInt(1, 7);

        // 步骤二 : 直接执行 sql 指令
        int count = stmt.executeUpdate();
        System.out.println("count = " + count);

    } catch (SQLException e) {

    } finally {
        // 3. 释放资源
        JDBCUtils.release(conn, stmt);
    }
}

猜你喜欢

转载自blog.csdn.net/zyrdfly/article/details/82694869
今日推荐