SQL注入问题及其解决办法

2021年4月23日 5点热度 0条评论 来源: 迷亭君

SQL注入问题及其解决办法

前几次的 JDBC 案列的数据库操作对象我一直使用的是 Statement 对象, 但是这个类实际上是存在一些问题的, 这就是我想要分享的 SQL 注入问题.

目录

SQL注入问题及其解决办法

问题发现

sql 注入的定义及其原因分析

问题解决------ PreparedStatement

PreparedStatement 和 Statement 比较

问题发现

前几天用 JDBC 编写一个从数据库获取用户名和密码登录的简单 java 程序发现了一个 bug

下面请看源代码:

// 连接数据库获取账号和密码登录
public class Test06 {
    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        PreparedStatement ps = null;
        try {
            Class.forName("com.mysql.jdbc.Driver");
            String url = "jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=true";
            String username = "root";
            String password = "XXXX";
            conn = DriverManager.getConnection(url, username, password);
            Scanner scanner = new Scanner(System.in);
            System.out.print("请输入用户名: ");
            String un = scanner.nextLine();
            System.out.print("请输入密 码: ");
            String pw = scanner.nextLine();

            // 下面代码存在 sql 注入问题
            String sql = "select * from user where name = '" + un
                    + "' and password = '" + pw + "'";
            stmt = conn.createStatement();
            // 下面这个代码的含义为: 将sql语句给DBMS, DBMS进行编译
            // 是先提交后编译, 所以如果用户提供了非法信息, 就导致了 sql 语句含义被扭曲
            // 从而出现不符合用户需求的情况
            rs = stmt.executeQuery(sql);
            if (rs.next()) {
                System.out.println("登录成功");
            }else {
                System.out.println("登录失败");
            }

        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                if (rs != null) {
                    rs.close();
                }
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
            try {
                if (stmt != null) {
                    stmt.close();
                }
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
            try {
                if (conn != null) {
                    conn.close();
                }
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
    }
}

user 表的内容是: 

当我输入用户名为 "xxx"  密码为 "XXX' or '1' = '1" 时, 也可以显示登录成功, 但是根据上面显示, 数据库中显然没有这个账户和密码. 

 

根据这一句代码

String sql = "select * from user where name = '" + un + "' and password = '" + pw + "'";

 我们还原一下 sql 语句, 拼接后的 sql 语句为 

select * from user where name = 'xxx' and password = 'XXX' or '1' = '1'

我们在 MySQL 中执行一下这句代码, 结果如下

由于 '1' = '1' 恒成立, MySQL 将这个表的所有信息都查找出来了, 显然这是不合理的.

在实际的工程中, 如果有别有用心之人投机取巧, 那么这个数据库中的信息是不安全的.

sql 注入的定义及其原因分析

sql 注入, 简单的讲就是, 用户的输入导致 sql 语句的含义改变, 从而导致一些不符合预期的情况出现.

我们再往深处分析, 这种现象是因为用户提供的信息和原本的 sql 语句框架进行了拼接, 之后将这个合成的 sql 语句交给 DBMS 进行编译才产生了这样的现象.

如果我们可以事先将 sql 语句的框架编译, 然后将用户的信息进行文本层面的拼接或者替换, 就不会产生这种问题了.

问题解决------ PreparedStatement

上面我们分析了问题的原因, 以及解决方法, 刚好 java 中提供了这样一个类 PreparedStatement .

Prepared 的意思是准备好的, 预编译的, 这个数据库操作对象的意思为 预先编译过的数据库操作对象

使用方法如下:

PreparedStatement ps = conn.prepareStatement(String sql);

conn 是数据库连接对象

sql 是 sql 语句框架

这个对象(PreparedStatement)的原理是: 预先对 sql 语句的框架进行编译, 然后再给 sql 语句进行传值.

下面给出例子:

String sql = "select * from user where name = ? and password = ?";
// 下面一行执行完, DBMS会将框架先编译好
PreparedStatement ps = conn.prepareStatement(sql);

其中 ? 为占位符, 代表这个位置将来可以替换

需要注意的是 PreparedStatement 的父类为 Statement , 所以它重写了父类中的 set 方法

下面讲解一下它的 set 方法: 例如

setSting(index, str) 为第 index 个 ? 获取字符串 str 填入, 会在传入的字符串两边加上单引号(第1个 ? 下标为1, 第2个 ? 下标为2, JDBC 中的下标都是从1开始的)

同理 setInt(index, int) 为获取 int 型的数据替换 ? 填入

下面我们给出改进后的代码:

import java.sql.*;
import java.util.Scanner;

// 连接数据库获取账号和密码登录
public class Test06 {
    public static void main(String[] args) {
        Connection conn = null;
//        Statement stmt = null;
        ResultSet rs = null;
        PreparedStatement ps = null;
        try {
            Class.forName("com.mysql.jdbc.Driver");
            String url = "jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=true";
            String username = "root";
            String password = "xxxx";
            conn = DriverManager.getConnection(url, username, password);
            Scanner scanner = new Scanner(System.in);
            System.out.print("请输入用户名: ");
            String un = scanner.nextLine();
            System.out.print("请输入密 码: ");
            String pw = scanner.nextLine();

            // 下面代码存在 sql 注入问题
//            String sql = "select * from user where name = '" + un
//                    + "' and password = '" + pw + "'";
//            stmt = conn.createStatement();
//            // 下面这个代码的含义为: 将sql语句给DBMS, DBMS进行编译
//            // 是先提交后编译, 所以如果用户提供了非法信息, 就导致了 sql 语句含义被扭曲
//            // 从而出现不符合用户需求的情况
//            rs = stmt.executeQuery(sql);

            // 改进
            // sql 注入是因为用户提供的信息也参与编译了
            // 只要用户的信息不参与编译, 问题就解决了
            // 数据库操作对象使用 PreparedStatement
            // 这个对象属于预编译的数据库操作对象
            // 原理是预先对 sql 语句的框架进行编译, 然后再给 sql 语句进行传值
            // 下面的字符串为 sql 语句的框架 其中?为占位符
            // 一个?将来接受一个值, 注意 占位符不能用单引号引起来
            String sql = "select * from user where name = ? and password = ?";
            // 下面一行执行完, DBMS会将框架先编译好
            ps = conn.prepareStatement(sql);
            // 下面给?传值, 第1个?下标为1, 第2个?下标为2, JDBC 中的下标都是从1开始的
            // setSting(index, str) 为,为第index个?获取字符串str填入,会在传入的字符串两边加上单引号
            // 所以不用?两边不用加单引号
            // setInt(index, int) 为获取int型的数据
            ps.setString(1, un);
            ps.setString(2, pw);
            // 执行 sql 语句, 下面的函数就不用传入 sql 语句了, 因为此时的数据库操作对象中已经有了sql的信息
            rs = ps.executeQuery();

            if (rs.next()) {
                System.out.println("登录成功");
            }else {
                System.out.println("登录失败");
            }

        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                if (rs != null) {
                    rs.close();
                }
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
            try {
//                if (stmt != null) {
//                    stmt.close();
//                }
                if (ps != null) {
                    ps.close();
                }
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
            try {
                if (conn != null) {
                    conn.close();
                }
            } catch (SQLException throwables) {
                throwables.printStackTrace();
            }
        }
    }
}

测试一下: 

注入问题解决, nice.

PreparedStatement 和 Statement 比较

1. PreparedStatement 解决了注入问题, 而 Statement 没有解决注入问题

2. PreparedStatement 编译一次可以执行多次, 而 Statement 编译一次执行一次

3. PreparedStatement 会在编译期做类型的安全检查, 而 Statement 不会做类型的安全检查, 例如 setString(1, 100) 编译期就会报错.

综上所述, 大部分情况使用 PreparedStatement , 只有极少数情况下需要使用 Statement 

比如就是需要 sql 注入的时候, 就是要进行 sql 语句拼接的时候:

比如, 淘宝中的产品升序或者降序排列, 我们需要使用关键字 desc , 这里如果使用 ? 替换的方式将不会对这个关键字编译, 而且替换的是'desc', 就会出现问题

下面总结何时使用PreparedStatement 和 Statement

PreparedStatement : 单纯的进行sql语句传值, 则可以使用

Statement : 必须要sql注入, 必须进行sql语句拼接的时候使用这个对象

 

今天就分享到这里, 希望大家多多评论.

 

 

 

 

    原文作者:迷亭君
    原文地址: https://blog.csdn.net/qq_49044908/article/details/116073409
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。