首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

JavaEE 學習筆記:PreparedStatement 防 SQL 注入原理與參數綁定實戰

     編輯:Java 相關知識

PreparedStatement 是 JDBC 防禦 SQL 注入的核心利器,通過預編譯 + 參數綁定徹底隔離代碼與數據。本文從注入漏洞演示、PreparedStatement 核心機制、參數設置方法、實戰代碼模板、常見誤區五個維度,幫你徹底掌握安全數據庫操作。

SQL 注入漏洞復現

場景:用戶登錄,拼接 SQL 字符串。

// 危險代碼
String sql = "SELECT * FROM `master` WHERE `name`='" + name + "' AND `password`='" + password + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);

攻擊輸入:

  • 用戶名:liubei
  • 密碼:123' OR '1'='1

實際執行 SQL:

SELECT * FROM `master` WHERE `name`='liubei' AND `password`='123' OR '1'='1'

'1'='1' 恆真,繞過密碼驗證,登錄成功!

PreparedStatement 核心防禦機制

  1. 預編譯:SQL 模板發送給數據庫,數據庫解析、編譯、優化生成執行計劃,結構已固定。
  2. 參數佔位符:? 標記參數位置,非 SQL 語法一部分。
  3. 參數綁定:setXxx(index, value) 將值安全傳遞給數據庫,數據庫按值處理,不再解析為 SQL 片段。
  4. 類型安全:setInt、setString、setTimestamp 等強類型方法,自動處理轉義、格式化。

標準使用模板

String sql = "UPDATE pet SET health=?, love=? WHERE id=?"; // 參數用 ?
Connection conn = null;
PreparedStatement pstmt = null;
try {
    conn = DriverManager.getConnection(url, user, pass);
    pstmt = conn.prepareStatement(sql); // 預編譯
    
    pstmt.setInt(1, 1234);   // 第 1 個 ? → health
    pstmt.setInt(2, 88);     // 第 2 個 ? → love
    pstmt.setInt(3, 2);      // 第 3 個 ? → id
    
    int rows = pstmt.executeUpdate(); // DML 返回影響行數
    System.out.println("更新行數: " + rows);
} catch (SQLException e) {
    e.printStackTrace();
} finally {
    // 逆序關閉
    if (pstmt != null) try { pstmt.close(); } catch (SQLException ignore) {}
    if (conn != null) try { conn.close(); } catch (SQLException ignore) {}
}

參數設置方法速查

進階:批量操作與生成鍵

批量插入/更新

conn.setAutoCommit(false);
String sql = "INSERT INTO orders(user_id, amount) VALUES(?, ?)";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
    for (Order o : orders) {
        ps.setLong(1, o.getUserId());
        ps.setBigDecimal(2, o.getAmount());
        ps.addBatch();
    }
    int[] counts = ps.executeBatch(); // 返回每條影響行數
    conn.commit();
}

獲取自增主鍵

String sql = "INSERT INTO users(name) VALUES(?)";
try (PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
    ps.setString(1, "Alice");
    ps.executeUpdate();
    try (ResultSet rs = ps.getGeneratedKeys()) {
        if (rs.next()) {
            long id = rs.getLong(1);
            System.out.println("生成主鍵: " + id);
        }
    }
}

常見誤區與避坑

Java 類型set 方法對應 SQL 類型示例
StringsetString(index, val)VARCHAR, CHAR, TEXT
int / IntegersetInt(index, val)INT, SMALLINT
long / LongsetLong(index, val)BIGINT
double / DoublesetDouble(index, val)DOUBLE, DECIMAL
boolean / BooleansetBoolean(index, val)BIT, BOOLEAN
Date / LocalDatesetDate(index, val)DATE
Timestamp / LocalDateTimesetTimestamp(index, val)DATETIME, TIMESTAMP
byte[]setBytes(index, val)BLOB, VARBINARY
InputStreamsetBinaryStream / setBlobBLOB (大文件流式寫入)
ReadersetCharacterStream / setClobCLOB (大文本流式寫入)

SQL 拼接空格陷阱(實戰血淚)

// 錯誤:拼接處缺少空格
String sql = "SELECT * FROM a WHERE x=? AND" + "y=?"; 
// 實際 SQL:... WHERE x=? ANDy=? → 語法錯誤

// 正確:保留空格
String sql = "SELECT * FROM a WHERE x=? AND " + "y=?"; 
// 或用 StringBuilder 可讀性更好
String sql = new StringBuilder()
    .append("SELECT * FROM a WHERE x=? ")
    .append("AND y=? ")
    .toString();

總結

PreparedStatement 通過預編譯固定 SQL 結構 + 類型安全參數綁定,從根源上杜絕 SQL 注入。掌握標準模板、參數設置方法、批量操作、生成鍵獲取,並養成佔位符思維、空格意識、資源自動關閉習慣,是每個 Java 開發者的基本功。現代項目建議使用 MyBatis/JPA 等 ORM 框架,其底層同樣基於 PreparedStatement,並提供更高級的動態 SQL、映射、緩存能力。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
誤區後果正確做法
用 prepareStatement 但仍拼接字符串失去防注入保護所有動態值均用 ? 佔位符
LIKE 查詢拼接 %:name LIKE ? → setString(1, "%" + kw + "%")正確!通配符屬於參數值一部分在 Java 代碼拼接 %,而非 SQL 中
IN 子句動態參數數量不固定無法直接綁定集合動態生成 ?,?,? 佔位符字符串,循環 set
忽略 executeQuery vs executeUpdate 區別執行 DML 用 query 返回 ResultSet 為空SELECT 用 query,INSERT/UPDATE/DELETE 用 update
未關閉資源導致連接洩漏連接池耗盡,應用掛起try-with-resources (JDBC 4.1+) 或 finally 逆序關閉