SQL 注入是 Web 安全頭號殺手。本文通過用戶登錄驗證案例,對比 Statement 拼接 SQL 導致注入漏洞、PreparedStatement 參數化查詢徹底防禦的完整過程,含代碼、測試數據、攻擊載荷構造與原理解析。
CREATE TABLE users (
id INT PRIMARY KEY,
username VARCHAR(30),
password VARCHAR(30)
);
INSERT INTO users VALUES (1, 'wonderq', 'root');
INSERT INTO users VALUES (2, 'chenxin', 'sweet');
public void loginVulnerable(String username, String password) {
String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
System.out.println("執行 SQL: " + sql);
try (Connection con = ConnectionSource.getConnection();
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
if (rs.next()) System.out.println("登錄成功!");
else System.out.println("登錄失敗!");
} catch (SQLException e) { throw new RuntimeException(e); }
}
正常登錄:`loginVulnerable("wonderq", "root")` → 成功。
注入攻擊:`loginVulnerable("chenxin", "a' OR 'b'='b")`
生成 SQL:
SELECT * FROM users WHERE username = 'chenxin' AND password = 'a' OR 'b'='b'
結果:`WHERE` 條件恆真 (`'b'='b'` 為 true),**繞過密碼驗證直接登錄成功**!
public void loginSecure(String username, String password) {
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
System.out.println("執行 SQL: " + sql);
try (Connection con = ConnectionSource.getConnection();
PreparedStatement ps = con.prepareStatement(sql)) {
ps.setString(1, username); // 佔位符 1
ps.setString(2, password); // 佔位符 2
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) System.out.println("登錄成功!");
else System.out.println("登錄失敗!");
}
} catch (SQLException e) { throw new RuntimeException(e); }
}
`loginSecure("chenxin", "a' OR 'b'='b")`
結果:登錄失敗。
原理:PreparedStatement 預編譯階段已確定 SQL 邏輯結構 (`username = ? AND password = ?`),執行階段將用戶輸入 **作為字面量字符串** 綁定到參數,而非 SQL 片段。輸入 `"a' OR 'b'='b"` 被視為完整密碼字符串查找,數據庫中無此密碼,自然失敗。
| 維度 | Statement | PreparedStatement |
|---|---|---|
| SQL 構建 | 運行時字符串拼接 | 預編譯 + 參數綁定 |
| 邏輯結構 | 可被用戶輸入篡改 | 編譯期固定,不可變 |
| 注入風險 | 極高 | 無 (參數視為數據) |
| 性能 | 每次解析優化 | 預編譯複用執行計劃 |
| 適用場景 | 極簡腳本、DDL | **所有業務 DML 查詢** |