Java JDBC SQL注入原理与预防实战:从Statement到PreparedStatement

清欢
清欢
发布时间:2026-10-15 | 3人浏览过
原创

在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 有三个显著优势:

  1. 彻底阻断SQL注入路径;
  2. 由于使用了预编译的SQL缓冲区,多次执行同构语句性能更高;
  3. 代码结构更优雅,摒弃了易出错的字符串拼接模式。
特性StatementPreparedStatement
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安全编程的实践资料请关注本站相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表