PreparedStatement是如何防止SQL注入的
# 5.PreparedStatement是如何防止SQL注入的
# 数据准备
为了方便演示,我们创建一个user表来模拟登录的情况
USE learnjdbc;
CREATE TABLE user (
id BIGINT AUTO_INCREMENT NOT NULL,
name VARCHAR(50) NOT NULL,
password VARCHAR(50) NOT NULL,
PRIMARY KEY(id)
) Engine=INNODB DEFAULT CHARSET=UTF8;
INSERT INTO user (id, name, password) VALUES (1, 'peterjxl', '123456')
INSERT INTO user (id, name, password) VALUES (2, 'peter', '123456')
2
3
4
5
6
7
8
9
10
11
# 一个简单的登录逻辑
这里也说明下登录的逻辑:
- 用户输入用户名和密码
- Java程序收到,并执行SQL判断用户名密码是否存在
- 如果一致则认为通过
SQL逻辑如下:
select count(1) from user where name = '用户名' and password = '密码'
如果用户输入的是正确的用户名和密码(例如peterjxl和123456),则执行的SQL是:
select count(1) from user where name = 'peterjxl' and password = '123456'
执行结果是1,说明存在数据,并且用户名和密码都是正确的,认为通过了用户密码校验,登录成功。
相关代码如下:这里假设name和password是用户输入的变量。
String name = "peterjxl";
String password = "123456";
Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
Statement statement = conn.createStatement();
ResultSet rs = statement.executeQuery("select count(1) from user where name ='" + name + "' and password = '" + password + "'");
while (rs.next()){
if (0 != rs.getInt(1)){
System.out.println("登录成功");
}else{
System.out.println("登录失败");
}
}
2
3
4
5
6
7
8
9
10
11
12
# 使用Statement演示SQL注入
如果用户输入的是一个特殊的字符呢?
String name = "peterjxl";
String password = "1' or ''='";
Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
Statement statement = conn.createStatement();
ResultSet rs = statement.executeQuery("select count(1) from user where name ='" + name + "' and password = '" + password + "'");
while (rs.next()){
if (0 != rs.getInt(1)){
System.out.println("登录成功");
}else{
System.out.println("登录失败");
}
}
2
3
4
5
6
7
8
9
10
11
12
以上代码拼接SQL后,如下:
select count(1) from user where name = 'peterjxl' and PASSWORD = '1' or ''=''
也就是说,即使用户不知道密码,只要使用SQL注入,就可以登录成功。
更糟糕的情况是,如果用户直接输入了一些删除表和数据库的语句,一旦被执行,后果不堪设想!例如他们可以这样拼接一大串字符:
select count(1) from user where name = 'peterjxl' and PASSWORD = '1' or ''='';
delete from user;
select count(1) from user where ''=''
2
3
# 使用PreparedStatement演示SQL注入
如果我们使用的是PreparedStatement呢?能否SQL注入?我们来试试看:为了方便,第8行我们打印执行的SQL
String name = "peterjxl";
String password = "1' or ''='";
Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
PreparedStatement stapreparedStatementement = conn.prepareStatement("select count(1) from user where name = ? and password = ?");
stapreparedStatementement.setObject(1, name);
stapreparedStatementement.setObject(2, password);
ResultSet rs = stapreparedStatementement.executeQuery();
System.out.println("sql = " + stapreparedStatementement.toString());
while (rs.next()){
if (0 != rs.getInt(1)){
System.out.println("登录成功");
}else{
System.out.println("登录失败");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
运行结果如下:
sql = com.mysql.cj.jdbc.ClientPreparedStatement: select count(1) from user where name = 'peterjxl' and password = '1'' or ''''='''
登录失败
2
为什么PreparedStatement能防止SQL注入?因为它会把用户输入的单引号做转义,从而防止SQL注入。
# PreparedStatement的源码
PreparedStatement只是一个接口,具体的实现类得看数据库厂商实现的源码是怎么样的,这里以MySQL驱动为例,观察其源码。
如果你使用IDE,可以直接在IDE里查看源码,这里以IDEA为例:
然后我们可以打断点调试,注意fillSendPacket
方法,将sql转换为字节流的过程中,给占位符中包含的单引号',额外加上一个单引号。
接下来我们继续debug,到底是哪个操作加上了单引号:
找到 buligComQuery()方法,有如下代码,其中:
staticSqlStrings[]
:存储的是以占位符?
划分开的sql语句转换的字节数组,不包含参数,即
staticSqlStrings[0]
=SELECT * FROM staff_info a WHERE a.staff_age =
staticSqlStrings[1]
=and a.staff_sex =
bindValues[]
:绑定的参数数组
从以上源码可以看出,sql语句和参数共同组装成 sendPacket
发给远程数据库做执行,而防SQL注入的处理,肯定就在 writeAsText()
流程中;
我们继续往下,来到 com.mysql.cj.protocol.a.StringValueEncoder
类中,该类用于处理String参数,于是可以定位到在将参数转换为字节数组的 getBytes()
方法中对输入的字符串进行了 StringUtils.escapeString()
处理:
既然已经发现字符串的工具类调用,不出意外我们马上就要找到答案了
可以看到方法中对各类特殊字符都做了转义处理,常规的添加斜杠 \ 如换行符\n ;而我们这里涉及的单引号 ' ,转义时会再添加一个 ', 这也解释了上述最终执行sql 的生成。
注意,不同版本的数据库驱动,代码可能有点不同:
- MySQL 5的驱动是加上反斜杠
\
做转义; - MySQL 8 的驱动是给单引号 额外加上一个单引号做转义
但无论如何实现,都能防止SQL注入。
不同版本的MySQL驱动,都是开源的,读者可以去官网下载:MySQL :: MySQL Connector/J 8.0 Developer Guide :: 4.3 Installing from Source (opens new window)
# 总结
生产环境一定要用PreparedStatement,而不是用Statement!
参考:
java中PreparedStatement和Statement详细讲解_程宇寒的博客-CSDN博客 (opens new window):本文主要参考了这位大佬的博客,并且分析了MySQL5驱动中PreparedStatement防止SQL注入的源码
PrepareStatement用法(附源码解析)_preparestatement的用法_DunkingCurry的博客-CSDN博客 (opens new window):分析了MySQL 8驱动中PreparedStatement防止SQL注入的源码