一、SQL注入
- 这两者的区别有好几点,比如预编译,防止sql注入是其中最大的一点,这里通过几个例子和简单的解析来探究PreparedStatement是如何防止sql注入的。
二、数据库记录如下:
id | playName | playNo | team |
---|---|---|---|
1 | Kobe Brayent | 8 | laker |
2 | Lebron James | 23 | laker |
3 | Tim Duncan | 21 | spurs |
4 | leonard | 2 | raptors |
5 | Stephen Curry | 30 | warriors |
6 | Klay Thompson | 11 | warriors |
三、代码验证:
- 下面通过一段代码来查询该数据库,通过传入一个正确的名称和一个伪造的包含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注入。