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

PreparedStatement 核心原理與防 SQL 注入實戰:從 Statement 陷阱到參數化查詢最佳實踐

     編輯:Java 綜合教學

前言:一個經典的登錄漏洞

數據庫表 `master` (寵物主人):

name     | password
---------|---------
liubei   | 123

脆弱代碼:Statement 拼接 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'
  • 結果:`WHERE` 條件恆真,繞過驗證直接登錄!

根因:用戶輸入被直接拼接進 SQL 字符串,改變了語義結構。這就是 SQL 注入。

PreparedStatement:參數化查詢的救世主

核心區別

特性StatementPreparedStatement
SQL 處理字符串拼接,每次發送完整 SQL預編譯模板,參數佔位符 ?
編譯次數每次執行都編譯預編譯一次,多次執行復用計劃
參數傳遞字符串拼接 (不安全)setXxx(index, value) 類型安全綁定
防注入無徹底防禦 (參數不參與語法解析)
性能低 (重複編譯、無計劃緩存)高 (預編譯、計劃緩存、批量優化)

正確使用姿勢

String sql = "SELECT * FROM `master` WHERE `name` = ? AND `password` = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);

pstmt.setString(1, name);     // 索引從 1 開始
pstmt.setString(2, password);

ResultSet rs = pstmt.executeQuery();
// 即使 password = "123' OR '1'='1",也會作為字面量字符串匹配,不會改變 SQL 結構

PreparedStatement 完整操作指南

1. 參數設置方法對照表

Java 類型Setter 方法數據庫類型示例
StringsetString(int, String)VARCHAR, CHAR, TEXT
int / IntegersetInt(int, int)INT, SMALLINT
long / LongsetLong(int, long)BIGINT
double / DoublesetDouble(int, double)DOUBLE, DECIMAL
boolean / BooleansetBoolean(int, boolean)BIT, BOOLEAN
Date / LocalDatesetDate(int, Date) / setObject(int, LocalDate)DATE
Timestamp / LocalDateTimesetTimestamp(int, Timestamp) / setObject(int, LocalDateTime)DATETIME, TIMESTAMP
byte[]setBytes(int, byte[])BLOB, VARBINARY
InputStreamsetBlob(int, InputStream) / setBinaryStreamBLOB
ReadersetClob(int, Reader) / setCharacterStreamCLOB, TEXT
null (任意類型)setNull(int, sqlType)所有類型

2. 三大執行方法

// 1. 查詢 (SELECT)
ResultSet rs = pstmt.executeQuery();

// 2. 更新 (INSERT/UPDATE/DELETE/DDL)
int affectedRows = pstmt.executeUpdate(); // 返回影響行數

// 3. 通用 (可執行任意 SQL,返回 boolean)
boolean hasResultSet = pstmt.execute(); // true: 有 ResultSet (SELECT); false: 更新計數或 DDL

3. 批量處理:性能倍增器

String sql = "INSERT INTO `order` (user_id, amount, create_time) VALUES (?, ?, ?)";
PreparedStatement pstmt = conn.prepareStatement(sql);
conn.setAutoCommit(false); // 關閉自動提交,手動控制事務

for (Order order : orderList) {
    pstmt.setLong(1, order.getUserId());
    pstmt.setBigDecimal(2, order.getAmount());
    pstmt.setTimestamp(3, new Timestamp(order.getCreateTime().getTime()));
    pstmt.addBatch(); // 累加到批次
    
    // 可選:分批提交,防止內存溢出
    if (++count % 1000 == 0) {
        pstmt.executeBatch();
        conn.commit();
        pstmt.clearBatch();
    }
}

int[] results = pstmt.executeBatch(); // 執行剩餘批次
conn.commit();

批量優化關鍵:關閉自動提交 + `addBatch()` + `executeBatch()` + 定期提交。MySQL 需連接串添加 `rewriteBatchedStatements=true` 開啟驅動端重寫優化。

進階:預編譯機制原理

// 1. Client 發送 prepareStatement(sql)
// 2. Server 解析、編譯、優化 → 生成執行計劃 → 分配 Statement ID (handle)
// 3. Server 返回 handle 給 Client
// 4. Client 發送 execute(handle, parameters)
// 5. Server 綁定參數、執行計劃、返回結果
// 6. 重複 4-5,無需重新解析編譯

服務端準備語句緩存

  • MySQL: `prepared_stmt_cache_size` (默認 256),LRU 淘汰
  • Oracle: `session_cached_cursors`
  • PostgreSQL: `prepared_statements` (會話級)
  • 超過緩存限制會導致重新編譯,影響性能

防 SQL 注入完整體系

  1. 核心原則:永不拼接 SQL 字符串,所有動態參數使用 PreparedStatement / MyBatis `#{}` / JPA 參數綁定
  2. 框架層保障:
    • MyBatis: `#{param}` → PreparedStatement;`${param}` → 字符串替換 (危險,僅用於表名/列名/排序字段且需白名單校驗)
    • JPA/Hibernate: 參數綁定自動參數化
    • JdbcTemplate: `?` 佔位符自動綁定
  3. 輸入校驗:白名單校驗 (枚舉值、正則)、長度限制、類型轉換
  4. 最小權限:應用數據庫用戶僅擁有必要表的增刪改查權限,無 DDL/DCL 權限
  5. WAF/防火牆:部署 ModSecurity、云 WAF 攔截明顯注入特徵
  6. 審計日志:記錄所有執行 SQL (含參數),事後追溯

常見誤區與避坑

誤區正確認知
以為 `PreparedStatement` 只是防注入核心優勢是預編譯復用執行計劃,性能提升往往超過安全收益
在 `IN` 子句中直接拼接列表`WHERE id IN (?)` 無法綁定列表。需動態生成 `?,?,?` 或使用 MyBatis ``
表名/列名/排序字段用參數綁定標識符不支持參數化。必須白名單校驗後拼接
批量插入不關閉自動提交每條 `executeUpdate` 都提交事務,性能極差。必須 `setAutoCommit(false)` + `executeBatch()`
長連接不關閉 PreparedStatement佔用服務端緩存槽位,導致 `PreparedStatement cache` 失效。用完即關或使用連接池 `preparedStatementCacheSize`

總結

PreparedStatement 以預編譯 + 參數化綁定雙重機制,同時解決了 SQL 注入安全性 與 重複編譯性能 兩大核心痛點。掌握其正確用法 (參數索引從 1 開始、批量處理關閉自動提交、標識符白名單校驗),並理解驅動端緩存與數據庫端計劃緩存的交互原理,是 Java 開發者從「能跑 SQL」進化到「寫好 SQL」的必修課。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved