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 注入的源码