当前位置:主页 > 查看内容

程序小生的一天--李四在JDBC出现的sql注入问题

发布时间:2021-05-07 00:00| 位朋友查看

简介:星期二风和日丽阳光明媚 李四穿上了一件崭新的白色格子衫背起了一个黑色的双肩背包踏着小巧的共享单车高高兴兴得来到了学校继续学习。 他打开了他昨天写的JDBC的入门代码犹如小精灵般在舞蹈他一脸的满足。 他想测试一下自己这个代码有一个需求通过JDBC想要实……

星期二,风和日丽,阳光明媚

李四,穿上了一件崭新的白色格子衫,背起了一个黑色的双肩背包,踏着小巧的共享单车,高高兴兴得来到了学校继续学习。

他打开了他昨天写的JDBC的入门代码,犹如小精灵般在舞蹈,他一脸的满足。

他想测试一下自己这个代码,有一个需求:通过JDBC想要实现用户登录时的用户名和密码的校验工作。

于是,他自然的写出了下面的代码:

public class DemoTest2 {
    public static void main(String[] args) throws SQLException, ClassNotFoundException {
        //加载驱动
        Class.forName("com.mysql.jdbc.Driver");
        //获得连接
        String url = "jdbc:mysql://localhost:3306/test";
        String user = "root";
        String password = "root";
        Connection con = DriverManager.getConnection(url, user, password);//获得数据库连接
        //获得Statement实现CRUD操作
        Statement sta = con.createStatement();
        //定义Sql语句
        String name = "lisi";
        String pass = "1234";
        String sql = "SELECT COUNT(*) as 个数 FROM login_user WHERE username ='" + name + "'and password ='"+pass+"'";;//校验操作

        //执行sql语句
        ResultSet resultSet = sta.executeQuery(sql);
        // 获取结果集的元数据
        ResultSetMetaData rsmd = resultSet.getMetaData();
        // 获取结果集的列数
        int columnCount = rsmd.getColumnCount();
        if (resultSet.next()) {
            for (int i = 0; i < columnCount; i++) {
                // 1. 获取列的别名
                String columnName = rsmd.getColumnLabel(i + 1);
                // 2. 根据列名获取对应数据表中的数据
                Object columnVal = resultSet.getObject(columnName);
                System.out.println(columnName + ":" + columnVal);
            }
        }
        //关闭资源
        resultSet.close();
        sta.close();
        con.close();

    }
}

运行结果:

李四高兴的发现,结果为1,说明了找到了结果,那么就意味着数据库有这条数据,就可以正常登录了

如果将密码变量’pass‘语句改为:123456

运行结果:

那么意味着用户输入的密码有误,校验失败,不能登录!

于是李四将这段代码告诉了同桌小王,一脸的炫耀。

同桌小王,看了看他写的代码,淡然撇了他一眼,冷哼一下,然后接过李四的电脑,改了一行代码,然后李四脸上的笑容凝固了起来。

        //定义Sql语句
        String name = "";
        String pass = "1' or '1'='1 ";
        String sql = "SELECT COUNT(*) as 个数 FROM login_user WHERE username ='" + name + "'and password ='"+pass+"'";//校验操作

改后,在进行再查询,发现count(*)的结果是有的

那么意味着,用户没有输入用户名和密码则可以进去,小王就只是在后面加了"1' or '1'='1 ",按照Sql语句的语法,我们发现无论前面怎么改,后面的"or ‘1’=‘’1"始终为真,那么这个语句始终是可以查询到数据的,那么意味着别人不需要用户名和密码就能“骗过”程序,进行正常的登录。

李四一脸疑惑,问小王为什么呢?小王淡然告诉他:“这是Sql注入问题,你自己再去查查相关资料了解一下再说吧!”;

于是,李四带着这个问题去查看了相关资料,发现了什么是Sql注入问题!

SQL注入:即是指web应用程序对用户输入数据的合法性没有判断或过滤不严,攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息。

李四,恍然大悟!原来,关键在这里。

那么又有一个问题,如何用JDBC相关知识解决呢?他继续查找资料。

他发现:只要用户提供信息不参与SQL语句的编译过程,问题就解决了。即使用户提供的信息中含有SQL语句的关键字,但是没有参与编译,不起作用。

他又发现一个对象:PreparedStatement 对象

1.PreparedStatement 实例包含已编译的 SQL 语句。这就是使语句“准备好”。包含于 PreparedStatement 对象中的 SQL 语句可具有一个或多个 IN 参数。IN参数的值在 SQL 语句创建时未被指定。相反的,该语句为每个 IN 参数保留一个问号(“?”)作为占位符。每个问号的值必须在该语句执行之前,通过适当的setXXX 方法来提供。

2.由于 PreparedStatement 对象已预编译过,所以其执行速度要快于 Statement 对象。因此,多次执行的 SQL 语句经常创建为 PreparedStatement 对象,以提高效率。

3.作为 Statement 的子类,PreparedStatement 继承了 Statement 的所有功能。另外它还添加了一整套方法,用于设置发送给数据库以取代 IN 参数占位符的值。同时,三种方法 execute、 executeQuery 和 executeUpdate 已被更改以使之不再需要参数。这些方法的 Statement 形式(接受 SQL 语句参数的形式)不应该用于 PreparedStatement 对象。

?

了解了相关概念后,我们来进行一个实际的操作,代码如下:

        //加载驱动
        Class.forName("com.mysql.jdbc.Driver");
        //获得连接
        String url = "jdbc:mysql://localhost:3306/test";
        String user = "root";
        String password = "root";
        Connection con = DriverManager.getConnection(url, user, password);
        //定义Sql语句
        String sql = " SELECT COUNT(*) as 个数 FROM login_user WHERE username = ? AND PASSWORD =?";
        //获得PreparedStatement实现CRUD操作
        PreparedStatement sta = con.prepareStatement(sql); //<------
        //给sql中的?号赋值
        sta.setString(1, "lisi");//<------
        sta.setString(2, "1234");//<------
        //执行sql语句
        ResultSet resultSet = sta.executeQuery();//<-----
        // 获取结果集的元数据
        ResultSetMetaData rsmd = resultSet.getMetaData();
        // 获取结果集的列数
        int columnCount = rsmd.getColumnCount();
        if (resultSet.next()) {
            for (int i = 0; i < columnCount; i++) {
                // 1. 获取列的别名
                String columnName = rsmd.getColumnLabel(i + 1);
                // 2. 根据列名获取对应数据表中的数据
                Object columnVal = resultSet.getObject(columnName);
                System.out.println(columnName + ":" + columnVal);
            }
        }
        //关闭资源
        resultSet.close();
        sta.close();
        con.close();

运行结果:

那么,如果同样如果修改刚才的用户名和密码,如下

        //给sql中的?号赋值
        sta.setString(1, "");
        sta.setString(2, "1' or '1'='1");

结果为:

李四发现这样依靠字符串拼接的sql注入问题就得到了解决。

他做出一个总结:不难发现,原来PreparedStatement对象会分别将参数和SQL交给MySQL 服务器,执行之前会进行预编译,在预编译阶段会检测SQL是否正确,正确则将编译结果直接提交给SQL服务器执行,编译失败则直接返回,不在执行PreparedStatement。

至于更底层的原理其实是prepareStatement对象防止sql注入的方式是把用户非法输入的单引号用\反斜杠做了转义,从而达到了防止sql注入的目的。

最后,李四在昨天学习JDBC流程的基础上总结如下流程图:

(本节完)

?

?

?

?

;原文链接:https://blog.csdn.net/Study_every/article/details/115115133
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!

推荐图文


随机推荐