JDBC语句与PreparedStatement – SQL注入示例
今天,我们将研究JDBC语句vs PreparedStatement和一些SQL注入示例。
在使用JDBC进行数据库连接时,我们可以使用Statement
或者PreparedStatement
来执行查询。
这些查询可以是CRUD操作查询,甚至可以是创建或者删除表的DDL查询。
语句与PreparedStatement
在比较Statement和PreparedStatement之前,让我们看看为什么应该避免使用JDBC Statement。
JDBC语句存在一些主要问题,在所有情况下都应避免使用JDBC语句,让我们以一个简单的示例来看一下。
我的本地MySQL数据库中有用户表,其中包含以下数据。
下面的脚本将创建表并插入数据以供测试使用。
CREATE TABLE `Users` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(20) NOT NULL DEFAULT '', `email` varchar(20) NOT NULL DEFAULT '', `country` varchar(20) DEFAULT 'USA', `password` varchar(20) NOT NULL DEFAULT '', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8; INSERT INTO `Users` (`id`, `name`, `email`, `country`, `password`) VALUES (1, 'hyman', '[email protected]', 'San Franceco', 'hyman123'), (4, 'David', '[email protected]', 'USA', 'david123'), (5, 'Raman', '[email protected]', 'UK', 'raman123');
一个实用程序类,用于创建与我们的mysql数据库的JDBC连接。
DBConnection.java
package com.theitroad.jdbc.statements; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBConnection { public final static String DB_DRIVER_CLASS = "com.mysql.jdbc.Driver"; public final static String DB_URL = "jdbc:mysql://localhost:3306/UserDB"; public final static String DB_USERNAME = "hyman"; public final static String DB_PASSWORD = "hyman123"; public static Connection getConnection() throws ClassNotFoundException, SQLException { Connection con = null; //load the Driver Class Class.forName(DB_DRIVER_CLASS); //create the connection now con = DriverManager.getConnection(DB_URL, DB_USERNAME, DB_PASSWORD); System.out.println("DB Connection created successfully"); return con; } }
现在,假设我们有一个下面的类,要求用户输入电子邮件ID和密码,如果匹配,则打印用户详细信息。
我正在使用JDBC语句执行查询。
GetUserDetails.java
package com.theitroad.jdbc.statements; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Scanner; public class GetUserDetails { public static void main(String[] args) throws ClassNotFoundException, SQLException { //read user entered data Scanner scanner = new Scanner(System.in); System.out.println("Please enter email id:"); String id = scanner.nextLine(); System.out.println("User id="+id); System.out.println("Please enter password to get details:"); String pwd = scanner.nextLine(); System.out.println("User password="+pwd); printUserData(id,pwd); } private static void printUserData(String id, String pwd) throws ClassNotFoundException, SQLException { Connection con = null; Statement stmt = null; ResultSet rs = null; try{ con = DBConnection.getConnection(); stmt = con.createStatement(); String query = "select name, country, password from Users where email = '"+id+"' and password='"+pwd+"'"; System.out.println(query); rs = stmt.executeQuery(query); while(rs.next()){ System.out.println("Name="+rs.getString("name")+",country="+rs.getString("country")+",password="+rs.getString("password")); } }finally{ if(rs != null) rs.close(); stmt.close(); con.close(); } } }
让我们看看当我们向上述程序传递不同类型的输入时会发生什么。
有效用户:
Please enter email id: [email protected] User [email protected] Please enter password to get details: david123 User password=david123 DB Connection created successfully select name, country, password from Users where email = '[email protected]' and password='david123' Name=David,country=USA,password=david123
因此我们的程序运行良好,有效的用户可以输入其凭据并获取其详细信息。
现在,让我们看看黑客如何获得未经授权的用户访问权限,因为我们正在使用Statement执行查询。
SQL注入
Please enter email id: [email protected]' or '1'='1 User [email protected]' or '1'='1 Please enter password to get details: User password= DB Connection created successfully select name, country, password from Users where email = '[email protected]' or '1'='1' and password='' Name=David,country=USA,password=david123
如您所见,即使没有密码,我们也可以获取用户详细信息。
这里要注意的关键点是查询是通过String串联创建的,如果我们提供正确的输入,我们可以对系统进行黑客攻击,就像在这里我们通过将用户ID传递为" [email protected]"或者" 1" =" 1"一样"。
这是SQL注入的一个示例,其中不良的编程导致我们的应用程序容易受到未经授权的数据库访问的影响。
一种解决方案是读取用户输入,然后转义MySQL使用的所有特殊字符,但这些字符笨拙且容易出错。
这就是JDBC API带有PreparedStatement
接口的原因,该接口扩展了Statement
并在执行查询之前自动转义特殊字符。
让我们使用PreparedStatement重写上面的类,然后尝试修改系统。
GetUserDetailsUsingPS.java
package com.theitroad.jdbc.statements; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Scanner; public class GetUserDetailsUsingPS { public static void main(String[] args) throws ClassNotFoundException, SQLException { //read user entered data Scanner scanner = new Scanner(System.in); System.out.println("Please enter email id:"); String id = scanner.nextLine(); System.out.println("User id=" + id); System.out.println("Please enter password to get details:"); String pwd = scanner.nextLine(); System.out.println("User password=" + pwd); printUserData(id, pwd); } private static void printUserData(String id, String pwd) throws ClassNotFoundException, SQLException { Connection con = null; PreparedStatement ps = null; ResultSet rs = null; String query = "select name, country, password from Users where email = ? and password = ?"; try { con = DBConnection.getConnection(); ps = con.prepareStatement(query); //set the parameter ps.setString(1, id); ps.setString(2, pwd); rs = ps.executeQuery(); while (rs.next()) { System.out.println("Name=" + rs.getString("name") + ",country=" + rs.getString("country") + ",password=" + rs.getString("password")); } } finally { if (rs != null) rs.close(); ps.close(); con.close(); } } }
现在,如果我们尝试破解该系统,那么看看会发生什么。
SQL注入
Please enter email id: [email protected]' or '1'='1 User [email protected]' or '1'='1 Please enter password to get details: User password= DB Connection created successfully
因此,我们无法破解数据库,因为发生的实际查询是:
select name, country, password from Users where email = '[email protected]\' or \'1\'=\'1\' and password=''
当我们触发要为关系数据库执行的查询时,它将经历以下步骤。
- 解析SQL查询
- SQL查询的编译
- 计划和优化数据采集路径
- 执行优化的查询并返回结果数据
当我们使用Statement
时,它会经历所有四个步骤,但是对于PreparedStatement
,创建准备好的语句时会执行前三个步骤。
因此,执行该查询所需的时间更少,并且该语句所需的时间更快。
使用PreparedStatement的另一个好处是我们可以通过addBatch()
和executeBatch()
方法使用批处理。
我们可以创建一个准备好的语句,然后使用它执行多个查询。
关于JDBC PreparedStatement要记住的几点是:
PreparedStatement帮助我们防止SQL注入攻击,因为它会自动转义特殊字符。
PreparedStatement允许我们使用参数输入执行动态查询。
PreparedStatement提供了不同类型的setter方法,以设置查询的输入参数。
PreparedStatement比Statement快。
当我们重复使用PreparedStatement或者将其批处理方法用于执行多个查询时,它会变得更加明显。PreparedStatement帮助我们使用setter方法编写面向对象的代码,而使用Statement,则必须使用String Concatenation创建查询。
如果要设置多个参数,则使用String串联编写Query看起来非常难看且容易出错。PreparedStatement返回
FORWARD_ONLY
ResultSet,因此我们只能向前移动。与Java数组或者列表不同,PreparedStatement变量的索引从1开始。
PreparedStatement的局限性之一是我们不能将其用于带有IN子句的SQL查询,因为PreparedStatement不允许我们为单个占位符(?)绑定多个值。
但是,几乎没有其他方法可以将PreparedStatement用于IN子句,有关更多信息,请参见JDBC PreparedStatement IN子句。
这就是JDBC语句与PreparedStatement的比较。
您应该始终使用PreparedStatement,因为它快速,面向对象,动态且更可靠。