Statement和PreparedStatement --- 防止sql注入

一、SQL注入

  • 这两者的区别有好几点,比如预编译,防止sql注入是其中最大的一点,这里通过几个例子和简单的解析来探究PreparedStatement是如何防止sql注入的。

二、数据库记录如下:

idplayNameplayNoteam
1Kobe Brayent8laker
2Lebron James23laker
3Tim Duncan21spurs
4leonard2raptors
5Stephen Curry30warriors
6Klay Thompson11warriors

三、代码验证:

  • 下面通过一段代码来查询该数据库,通过传入一个正确的名称和一个伪造的包含sql注入的名称去查询数据库,看Statement和PreparedStatement二者查询的区别。
     public static void main(String[] args) {
             String nameRight = "Lebron James"; //模拟用户输入正确的名称
             String fakeName = "Lebron Jamesxxx' or '1 = 1 "; //模拟用户输入错误的名称
             int resultOfRightNameStatement = searchByName(nameRight, false); //使用Statement查询正确的名字
             int resultOfFakeNameStatement = searchByName(fakeName, false); //使用Statement查询错误的名字
             int resultOfRightNamePs = searchByName(nameRight, true); //使用PreparedStatement查询正确的名字
             int resultOfFakeNamePs = searchByName(fakeName, true); //使用PreparedStatement查询错误的名字
             System.out.println("使用Statement查询正确的sql, 查询总数为:" + resultOfRightNameStatement);
             System.out.println("使用Statement查询错误的sql,查询总数为:" + resultOfFakeNameStatement);
             System.out.println("使用PreparedStatement查询正确的sql, 查询总数为:" + resultOfRightNamePs);
             System.out.println("使用PreparedStatement查询错误的sql,查询总数为:" + resultOfFakeNamePs);
         }
     
         public static int searchByName(String username, boolean safe) {
             int count = 0;
             try {
                 Class.forName("com.mysql.jdbc.Driver");
                 Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
                 String sql;
                 ResultSet rs = null;
                 if (safe) {
                     //使用PreparedStatement
                     sql = "SELECT * FROM tb_player where playName= ?";
                     PreparedStatement preparedStatement = conn.prepareStatement(sql);
                     preparedStatement.setString(1, username);
                     rs = preparedStatement.executeQuery();
                 } else {
                     //使用Statement
                     sql = "SELECT * FROM tb_player where playName='" + username + "'";
                     Statement statement = conn.createStatement();
                     rs = statement.executeQuery(sql);
                 }
                 if (rs != null) {
                     while (rs.next()) {
                         count++;
                     }
                 }
                 return count;
             } catch (SQLException e) {
                 e.printStackTrace();
             } catch (ClassNotFoundException e) {
                 e.printStackTrace();
             } finally {
                 // finally block used to close resources
                 try {
                     if (stmt != null)
                         stmt.close();
                 } catch (SQLException se2) {
                 }// nothing we can do
                 try {
                     if (conn != null)
                         conn.close();
                 } catch (SQLException se) {
                     se.printStackTrace();
                 }
             }
             return -1;
         }
        
        输出:
        打印sql:SELECT * FROM tb_player where playName='Lebron James'
        打印sql:SELECT * FROM tb_player where playName='Lebron Jamesxxx' or '1 = 1 '
        打印sql:com.mysql.jdbc.JDBC4PreparedStatement@2ef1e4fa: SELECT * FROM tb_player where playName= 'Lebron James'
        打印sql:com.mysql.jdbc.JDBC4PreparedStatement@5ce65a89: SELECT * FROM tb_player where playName= 'Lebron Jamesxxx\' or \'1 = 1 '
        使用Statement查询正确的sql, 查询总数为:1
        使用Statement查询错误的sql,查询总数为:6
        使用PreparedStatement查询正确的sql, 查询总数为:1
        使用PreparedStatement查询错误的sql,查询总数为:0
分析
  • 在代码示例中,我们这样设计接口:“SELECT * FROM t_user where user_name=’”+userName+"’";
    我们看到结果是:当时传入的参数包含恶意语句时,Statement可以执行且破坏了原有的逻辑,在username错误的情况下,因为后面的or 1=1恒成立导致查询到了数据.但是PrepareStatement就不会,查询到的是0。并且我们通过打印出来的sql语句也能够看到为什么前者不能防止sql注入,因为把or 1=1当做了一个查询条件,但是后者把参数中包含的单引号做了转义处理,结果sql语义就是查找用户名为"Lebron Jamesxxx’ or '1 = 1"的用户,显然是不存在这个用户的的,因此做到了防止sql注入,那么PrepareStatement底层是不是通过这样的转义来防止sql注入的呢?

溯源

  • 我们在mysql的驱动包的PrepareStatement类下面的setString里面设置断点,然后看他对于"Lebron Jamesxxx’ or '1 = 1"的输入参数是不是做了特殊字符的转义处理。
     for(int i = 0; i < stringLength; ++i) {
                        char c = x.charAt(i);
                        switch(c) {
                        case '\u0000':
                            buf.append('\\');
                            buf.append('0');
                            break;
                        case '\n':
                            buf.append('\\');
                            buf.append('n');
                            break;
                        case '\r':
                            buf.append('\\');
                            buf.append('r');
                            break;
                        case '\u001a':
                            buf.append('\\');
                            buf.append('Z');
                            break;
                        case '"':
                            if (this.usingAnsiMode) {
                                buf.append('\\');
                            }
    
                            buf.append('"');
                            break;
                        case '\'':
                            buf.append('\\');
                            buf.append('\'');
                            break;
                        case '\\':
                            buf.append('\\');
                            buf.append('\\');
                            break;
                        case '¥':
                        case '₩':
                            if (this.charsetEncoder != null) {
                                CharBuffer cbuf = CharBuffer.allocate(1);
                                ByteBuffer bbuf = ByteBuffer.allocate(1);
                                cbuf.put(c);
                                cbuf.position(0);
                                this.charsetEncoder.encode(cbuf, bbuf, true);
                                if (bbuf.get(0) == 92) {
                                    buf.append('\\');
                                }
                            }
                        default:
                            buf.append(c);
                        }
                    }
     
  • 看了这段代码我们就明白了,方法内部会遍历传入的参数,一个一个字符的处理,处理完之后拼接作为新的参数,在处理的过程中对很多类型的特殊字符都会做转义处理,比如单引号会转换为’,双引号会转化为" , 转换前后起到的效果就是将之前可能传入的恶意代码给屏蔽了,为什么这么说,还是按照之前的例子:输入的用户名字是下面这样一个字符串:
	"Lebron Jamesxxx' or '1 = 1 "

如果使用Statemnent没有做特殊处理直接拼接,拼接的就是这个部分: Lebron Jamesxxx’ or '1 = 1 ,原本的sql是:

	"SELECT * FROM tb_player where playName='" + username + "'";

那么拼接完成之后就是:

	SELECT * FROM tb_player where playName='Lebron Jamesxxx' or '1 = 1';

这就刚好把后面的1=1作为了 一个条件,这个语句永远成立。
我们再来看转义之后的,Lebron Jamesxxx’ or ‘1 = 1 转义就成了:Lebron Jamesxxx’ or '1 = 1 ,然后替换?的位置就是:

	SELECT * FROM tb_player where playName= 'Lebron Jamesxxx\' or \'1 = 1',

和我们之前打印出来的一样,这样子就相当于是查找用户为Lebron Jamesxxx’ or '1 = 1的记录,肯定是没有的,这里如果有其他的特殊字符也会转义,转义之后的特殊字符就会作为条件的一部分进行处理,而不会改变sql的语义,因此很大程度上防止了sql注入。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值