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命令的语法结构。于是,PreparedStatement在SQL命令执行效率和安全性方面较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命令的语句对象有Statement和PreparedStatement两个,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命令执行时,才在程序中通过PreparedStatement的setXXX()方法设置参数占位符?的值,然后用已编译好的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命令,于是,无需使用PreparedStatement的setXxx()方法设置参数占位符?的值,其执行效果与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,即:无论用户是否知道用户密码,用户都会登录成功。