Java JDBC SQL注入原理与预防实战:从Statement到PreparedStatement
在Java后端开发中,直接拼接用户输入构建SQL语句是引发“SQL注入”漏洞的高危行为。本文将剖析该漏洞的底层逻辑,并通过完整代码演示如何利用PreparedStatement的安全特性,从根本上杜绝SQL注入风险。
SQL注入的危害与原理
什么是SQL注入
SQL注入是一种利用Web应用程序对用户输入数据验证不严而产生的攻击手段。当用户在输入数据中夹杂了合法的SQL关键字或语法片段,且这些数据未加处理直接拼接到SQL语句中进行编译执行时,就会导致原本的查询逻辑被篡改。通常情况下,这种篡改会让条件表达式变为恒真(如 1=1),从而使攻击者能绕过身份验证提取甚至修改数据库内容。
注入效果演示
下面以一段典型的Java JDBC登录代码为例。在该实现中,程序接收控制台的输入后,通过字符串拼接的方式生成SQL:'"+user+"' AND pssword = '"+pwd+"'。
package cn.bdqn.demo03;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Scanner;
public class Login {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
//创建Scanner类对象,从控制台获取用户名和密码数据
Scanner sc = new Scanner(System.in);
System.out.println("请输入用户名:");
String user = sc.nextLine();//使用nextLine()方法获取字符串
System.out.println.println("请输入密码:");
String pwd = sc.nextLine();//使用nextLine()方法获取字符串
//1、注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2、获取连接对象
String url = "jdbc:mysql://127.0.0.1:3306/java221804";
String dbuser = "root";
String pssword = "123456";
Connection connection = DriverManager.getConnection(url, dbuser, pssword);
//3、获取发送SQL语句的对象
Statement statement =connection.createStatement();
//编写SQL语句(存在严重安全隐患)
String sql = "SELECT * FROM user WHERE username='"+user+"' AND pssword = '"+pwd+"';";
//4、执行SQL语句
ResultSet resultSet=statement.executeQuery(sql);
if(resultSet.next()){
System.out.println("用户名和密码正确,登录成功");
}else{
System.out.println("用户名或密码不正确,登录失败");
}
//6、关闭资源
resultSet.close();
statement.close();
connection.close();
sc.close();
}
}攻击场景复现
当输入正确的用户名和密码时,系统提示登录失败:
然而,如果攻击者在用户名框中输入恶意构造的字符,例如:
abc' or 1=1;#,而密码随便填写为 123,则最终发送给数据库执行的SQL将变为:
SELECT * FROM user WHERE username='abc' or 1=1;#' AND pssword = '123';
分析这段SQL可以发现:or 1=1使得条件部分永远为真;而后面的 # 符号在SQL中被视为注释符,导致其后的 AND pssword = ... 全部被忽略。因此,该查询会返回表中所有数据,由于第一条记录必然存在,程序的判断逻辑便会错误地认定“登录成功”。这就是典型的SQL注入攻击。
使用 PreparedStatement 防止注入
要避免SQL注入,核心思路是改变SQL的执行机制:不让用户输入的脏数据参与最终的SQL编译过程。通过使用 PreparedStatement 替代基础的 Statement 接口,可以实现这一目标。
PreparedStatement 利用了“预编译”技术。它先将带有参数的SQL模板发送给数据库服务器进行编译,建立好执行计划;随后才将实际的用户参数作为纯粹的值(而非代码)填充进去。这样一来,即使参数中包含 OR 或 # 等特殊字符,数据库也只会将其当作普通文本处理,绝不会将其解析为指令。
核心操作步骤
PrepareStatement的优势包括:
- 安全性高,自动规避SQL注入风险
- 效率较高,支持SQL语句的缓存复用
- 可动态填充数据,支持批量执行相同结构的语句
第1步:定义参数标记(?)
在书写SQL模板时,将所有需要由外部传入的数据位置替换为问号 ?:
// 注意:这里没有进行任何字符串拼接 String sql = "select * from user where userName = ? and pssword=?;";
第2步:创建PreparedStatement并绑定值
调用 prepareStatement 获取对象后,根据 ?占位符的顺序(索引从1开始),依次调用 setXxx() 方法进行参数绑定:
PreparedStatement preparedStatement = connection.prepareStatement(sql); // 绑定第1个?为userName,第2个?为pwd preparedStatement.setString(1, userName); preparedStatement.setString(2, pwd);
安全改造综合案例
下面是对上述登录接口的完整重写,体现了防御性的最佳实践:
package cn.bdqn.demo02;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Scanner;
public class PreparedStatementDemo01 {
public static void main(String[] args) throws ClassNotFoundException,SQLException {
Scanner sc = new Scanner(System.in);
System.out.println("请输入用户名:");
String userName = sc.nextLine();
System.out.println("请输入密码:");
String pwd = sc.nextLine();
// 1、注册驱动
Class.forName("com.mysql.jdbc.Driver");
// 2、获得连接
String url = "jdbc:mysql://localhost:3306/java2217";
String user = "root";
String pssword = "123456";
Connection connection = DriverManager.getConnection(url, user, pssword);
// 3、获取预处理对象,此时SQL已具备骨架
String sql = "select * from user where userName = ? and pssword=?;";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 4、绑定参数,有多少个?绑定多少个参数值
preparedStatement.setString(1, userName);
preparedStatement.setString(2, pwd);
// 5、执行SQL语句,并处理结果
ResultSet resultSet = preparedStatement.executeQuery();
if (resultSet.next()) {
System.out.println("用户名和密码正确,登录成功");
} else {
System.out.println("用户名或密码错误,登录失败");
}
// 6、释放资源:与关闭流的方式一样,先开的后关,后开的先关
resultSet.close();
preparedStatement.close();
connection.close();
sc.close();
}
}关键对比总结
PreparedStatement 相较于传统 Statement 有三个显著优势:
- 彻底阻断SQL注入路径;
- 由于使用了预编译的SQL缓冲区,多次执行同构语句性能更高;
- 代码结构更优雅,摒弃了易出错的字符串拼接模式。
| 特性 | Statement | PreparedStatement |
|---|---|---|
| SQL类型 | 静态SQL,需手动拼接 | 动态SQL,采用预编译 |
| 执行效率 | 较低,无缓存 | 较高,存在预编译缓存 |
| 安全性 | 易受攻击 | 天然免疫注入 |
何时仍需使用 Statement?
虽然绝大多数业务场景都应优先选用 PreparedStatement,但在一种特殊情况下必须回退使用 Statement:当SQL的结构本身(而非参数值)需要根据用户输入动态改变时。例如,让客户端选择表的升序或降序排列方式,因为这部分涉及SQL关键字(asc/desc),无法使用 ? 占位符代替。
public static void main(String[] args) {
Scanner s = new Scanner(System.in);
System.out.print("升序输入asc,降序输入desc:");
String order = s.nextLine();
// 定义变量
Connection connection = null;
Statement statement = null;
ResultSet rs = null;
try {
// 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 获取连接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/MyTest","root", "******");
// 获取数据库操作对象
statement = connection.createStatement();
// 此处必须拼接结构,因为排序规则是SQL语法的一部分
String sql = "select * from emp order by sal " + order;
rs = statement.executeQuery(sql);
// 处理查询结果集
while(rs.next()){
String ename = rs.getString("ename");
double sal = rs.getDouble("sal");
System.out.println("姓名:" + ename + ",薪资:" + sal);
}
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
} finally {
// 安全释放资源检查
if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } }
if (statement != null) { try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } }
if (connection != null) { try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } }
}
}以上就是本文关于Java JDBC中SQL注入原理及防御的详细解答。更多关于Java安全编程的实践资料请关注本站相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













