【原创】JDBC基础之(五)深入理解PreparedStatement

2020-04-30 18:01发布

JDBC基础之(五)深入理解PreparedStatement

JDBC API中,包含两个用于执行SQL命令的语句对象接口,一个是java.sql包中的Statement接口,另一个是该包中的PreparedStatement接口,PreparedStatement接口是Statement接口的子接口,虽然PreparedStatement继承自Statement,它们在使用方面,仍然存在不同,可以用PreparedStatement替代Statement,而不再使用Statement语句对象执行SQL命令。

Statement是执行静态SQL命令的接口,它负责将SQL命令发送到数据库执行,并接收数据库返回的执行结果。使用Statement执行SQL命令,数据库不会对SQL命令进行预编译,也不会存储SQL命令的执行计划,而是,一次编译一次执行,所以,较PreparedStatement而言,执行效率相对较低。

PreparedStatement是执行动态SQL命令的接口,它也可以替代Statement执行静态SQL命令,无论是动态SQL命令,还是静态SQL命令,PreparedStatement都会将SQL命令发送到数据库进行预先编译。

PreparedStatement负责将SQL命令发送到数据库,数据库接收到SQL命令后,会对SQL命令进行语义、语法检查、预编译,生成这个SQL命令的一个执行计划储存在数据库服务器中。当执行SQL命令时,若是动态SQL命令,需要先设置动态SQL命令中?占位符参数的值,然后执行已编译好的SQL命令执行计划,而不再对SQL进行编译了;只要SQL命令不变,若只改变?占位符的参数值,就无需重新编译SQL命令,相对于使用Statement对象每次都需要数据库编译SQL命令而言,执行效率更高。当数据库连接关闭时,SQL执行计划才从数据库中清除。

PreparedStatement还提供了为编译的动态SQL命令设置?占位符参数值的一系列方法,参数值必须在SQL命令执行之前设置

setXxx(int columnIndex, value)

使用setXxx系列方法传入的参数不会改变已预先编译好的SQL命令的语法结构。于是,PreparedStatementSQL命令执行效率和安全性方面较Statement更高,所以,通常用PreparedStatement代替Statement执行SQL命令,而不再使用Statement

 

下面,以SQL注入问题的处理,理解PreparedStatement在安全性方面较Statement更完善。

一、什么是SQL注入

SQL注入是一种在WEB应用中比较常见的黑客攻击方式,黑客利用后台数据库中SQL语句执行的安全漏洞,将一些特殊的SQL指令(例如: or 1=1)嵌入到正确的SQL语句中。

WEB应用程序为例,若在程序开发时,后台程序在向数据库发送SQL语句前,没有对SQL语句做语法检查,或者对特殊SQL指令做字符串检查,于是,程序就会将嵌入特殊SQL指令的SQL语句误认为是正确的SQL语句,发送到数据库执行,有可能从数据库中查询出不正确的数据结果,甚至有的会对数据库中的数据造成错误的增删改操作,于是,注入的SQL指令就达到了黑客攻击应用程序的目的。

二、一个常见的SQL注入场景

WEB应用中,一个常常发生SQL注入的场景是:用户登录验证

 

首先,用户在用户登录页面,输入用户名和密码,点击“登录”提交用户登录验证。

接着,后台Java程序接收前端页面中用户输入的用户名和密码,可以拼接一个如下所示的数据查询的SQL语句发送到数据库执行,检查用户是否存在:

select count(1) from customer where cname=’用户名and cpassword=’密码;

若用户名、密码正确,则查询结果的用户数大于0,用户登录成功;否则,用户数小于等于0,用户登录失败。

 

后台Java程序示例代码:

String userName = "章子怡";

String cpassword = "123';

String sql = "select count(1) from customer c where c.cname = '"

+ userName + "' and c.cpassword='" + cpassword + "'";

 

经过后台Java程序的SQL拼接处理,最终发送到数据库执行的SQL查询语句如下:

select count(1) from customer c where c.cname = '章子怡' and c.cpassword='123';

 

以上Java程序代码,拼接数据查询SQL语句,没有对SQL语法做检查,也没有对特殊字符做处理,例如:单引号  于是,用户输入一个万能密码123 or 1=1,即:

String cpassword = "123' or 1=1;

经过后台Java程序的SQL拼接处理,最终发送到数据库执行的SQL查询语句如下:

select count(1) from customer c where c.cname = '章子怡' and c.cpassword='123' or 1=1;

数据库在执行SQL语句时,逻辑运算符具有优先级,【=】优先于【and】,【and】优先于【or】,并具有传递性,于是以上SQL语句分成以下个部分执行,然后再or

select count(1) from customer c where c.cname = '章子怡' and c.cpassword='123'or 1=1

部分语句返回的boolean值进行逻辑or运算,无论前一部分SQL语句的执行结果是否为真因为1=1】恒为真,所以,这个SQL查询语句执行的结果一定大于0,即:无论用户是否知道用户密码,用户都会登录成功

在以上WEB后台程序中,不需要知道登录密码,黑客也可以模拟任何一个用户账户登录系统,进行操作。

三、使用PreparedStatement+动态SQL阻止SQL注入

JDBC中,用于执行SQL命令的语句对象有StatementPreparedStatement两个,Statement仅支持静态SQL命令,PreparedStatement是一种预编译的SQL语句对象,既可以支持静态SQL命令,也可以支持动态SQL命令。

JDBC中,常常使用动态SQL命令和PreparedStatement语句对象阻止SQL注入的发生。

动态SQL命令,是一种不包含具体参数值,而用?占位符来表示实际数据值的语句,例如:

select count(1) from customer c where c.cname = ? and c.cpassword = ?

PreparedStatement在执行动态SQL命令时,首先,将带参数占位符?的动态SQL命令发送到数据库,数据库接收到动态SQL命令后,会对SQL命令进行语义、语法检查、预编译,生成动态SQL命令的一个执行计划储存在数据库服务器中;其次,当SQL命令执行时,才在程序中通过PreparedStatementsetXXX()方法设置参数占位符?的值,然后用已编译好的SQL命令执行计划执行SQL语句,这样即使传入的参数值中包含有特殊字符,例如:123 or 1=1,传入的数据也无法改变原SQL语句已经预编译好的语法结构,于是,可以防止SQL注入的发生。

在用户登录后台Java程序中,使用PreparedStatement对象执行动态SQL命令select count(1) from customer c where c.cname = ? and c.cpassword = ? 使用万能密码123 or 1=1,通过调用setXXX()方法设置sql语句中的参数并赋值给?占位符,即使传入了特殊字符or 1=1,传入的数据也无法改变原SQL语句中已经预编译好的语法结构,于是,可以防止SQL注入的发生。

 

后台Java程序示例代码:

String userName = "章子怡";

String cpassword = "123' or 1=1;

String sql = select count(1) from customer c where c.cname = ? and c.cpassword = ?;

//加载驱动

Class.forName("com.mysql.cj.jdbc.Driver");

//创建连接

//从数据库属性文件jdbc.properties中获取数据库连接参数

Properties prop = new Properties();

InputStream is = JDBCTest.class.getClassLoader().

getResourceAsStream("jdbc.properties");

prop.load(is);

String url = prop.getProperty("url");

String user = prop.getProperty("user");

String password = prop.getProperty("password");

Connection con = DriverManager.getConnection(url, user, password);

//创建执行执行SQL命令的语句对象,执行SQL语句,处理执行结果

PreparedStatement pst = con.prepareStatement(sql);

pst.setString(1, userName);

pst.setString(2, cpassword);

ResultSet rs = pst.executeQuery();

if (rs != null){

rs.next();

int iCount = (int)rs.getString(1);

if (iCount > 0) {

System.out.println("用户登录成功");

} else {

System.out.println("用户登录未成功,请重新登录");

}}}

//关闭连接,释放资源

if (rs != null) rs.close();

if (pst != null) pst.close();

if (con != null)con5.close();

四、理解PreparedStatement是如何阻止SQL注入的

以用户登录验证为例,后台数据库使用MySQL,通过MySQL数据库操作日志,查看执行时的真实SQL命令,理解PreparedStatement是如何阻止SQL注入的。

1、开启MySQL数据库日志功能

MySQL数据库中,开启数据库日志功能,将执行的SQL命令记录在日志文件中。由于日志数据量非常大,MySQL数据通常不会开启日志功能,需要手动设置日志开启。

开启数据库日志功能:

SET GLOBAL general_log = 'ON';

显示数据库日志文件位置:

SHOW VARIABLES LIKE 'general_log%';

关闭数据库日志功能:

SET GLOBAL general_log = 'OFF';

 

 

2、使用静态SQL命令,无法阻止SQL注入

在后台程序代码中,使用静态SQL命令和PreparedStatement实现用户登录验证功能,由于使用的是静态SQL命令,于是,无需使用PreparedStatementsetXxx()方法设置参数占位符?的值,其执行效果与Statement类似。

String userName = "章子怡";

String cpassword = "123' or '1=1";

// 拼接静态SQL

String sql = "select count(1) from customer c where c.cname = '" + userName

+ "' and c.cpassword='" + cpassword + "'";

PreparedStatement pst = con.prepareStatement(sql);

ResultSet rs = pst.executeQuery();

查看数据库日志文件ibmwork-PC.log,执行的SQL命令如下:

 

以上代码,拼接数据查询SQL命令,没有对SQL语法做检查,也没有对特殊字符做处理,于是,用户输入一个万能密码123 or 1=1,这个SQL查询语句执行的结果一定大于0,即:无论用户是否知道用户密码,用户都会登录成功

3、使用动态SQL