2020-06-21
JAVAJDBCMYSQL

使用 PreparedStatement

Java 使用 PreparedStatement 的用意與使用方法。

PreparedStatement Usage

在不用 PreparedStatement 的情況下,我們對 DB 進行資料寫入的時候會這樣寫:

Statement stat = conn.createStatement();

stat.executeUpdate(
  "insert into MyGuests (firstname, lastname, email) " +
  "values ('Chen', 'Tom', 'tom@xxx.com');"
);

如果要用變數傳話會變成這樣:

Statement stat = conn.createStatement();
String firstName = "Chen";
String lastName = "Andy";
String email = "andy@xxx.com";

stat.executeUpdate(
  "insert into MyGuests (firstname, lastname, email) " +
  "values ('" + firstName +"', '" + lastName + "', '" + email + "');"
);

SQL Injection 問題

但是上面的用法會有被 SQL Injection 的問題。 什麼是 SQL Injection 呢? 簡單來個範例好了!

我們 DB Table 中有這些資料

+----+-----------+----------+----------------------+
| id | firstname | lastname | email                |
+----+-----------+----------+----------------------+
|  1 | Chang     | Wayne    | judysocute@gmail.com |
|  2 | Chang     | Andy     | judysocute@gmail.com |
|  3 | Chen      | Tom      | tom@xxx.com          |
|  4 | Chen      | Andy     | andy@xxx.com         |
|  5 | Chen      | Andy     | andy@xxx.com         |
+----+-----------+----------+----------------------+

我們現在有個 Query 的程式是這樣的

Statement stat = conn.createStatement();
String queryID = "1";
ResultSet resultSet = stat
    .executeQuery(
          "Select * From MyGuests Where id = '" + queryID + "'"
    );

// 那出來的結果會像是這樣:
// ================================
// 1
// Chang
// Wayne
// judysocute@gmail.com
// 2020-06-07
// ================================

但是呢!這個時候我們把 queryId 的值改掉, 改成這樣:

Statement stat = conn.createStatement();
String queryID = "' or 1 = '1";
ResultSet resultSet = stat
    .executeQuery(
          "Select * From MyGuests Where id = '" + queryID + "'"
    );

// 那出來的結果,就變成全部資料了:
//
// #實際執行的 SQL 語法:
// Select * From MyGuests Where id = '' or 1 = '1';
//
// ================================
// 1
// Chang
// Wayne
// judysocute@gmail.com
// 2020-06-07
// ================================
// 
// ... 略過 ...
// 
// ================================
// 5
// Chen
// Andy
// andy@xxx.com
// 2020-06-21
// ================================

當然!這不一定會發生,但是有這個風險存在我們就不能放任不管。

心法: 永遠不要相信使用者的輸入。

使用 PreparedStatement

當我們要使用 PreparedStatement 的時候,我們的 Statement 物件就要改用 Connection 的 prepareStatement() 方法來取得。

// Statement stat = conn.createStatement(); 之前舊的用法
PreparedStatement stat = conn
    .prepareStatement("Select * From MyGuests Where id = ?");

可以看到在查詢參數部分,我們用 ? 來表示 等需要用到的時候我們再使用相對應 setInt()setString() 等方法來指定 ? 的真正值:

PreparedStatement stat = conn
    .prepareStatement("Select * From MyGuests Where id = ?");

stat.setString(1, "2");
stat.executeQuery();
stat.clearParameters();

這邊 setString() 方法的第一個參數是 ? 的位置,第二個參數是要所代表的值。

而設定完查詢語法時,只要執行 executeQuery()executeUpdate() 就可以讓 SQL 生效了。

使用完之後,如果要重複使用這個 PreparedStatement 的話,可以使用 clearParameters() 方法將將之前指定的值給清空,就可以繼續使用了。

再來個 insert 的用法吧:

PreparedStatement stat = conn
.prepareStatement(
  "insert into MyGuests (firstname, lastname, email) values (?, ?, ?);"
);

stat.setString(1, "Wang");
stat.setString(2, "Jack");
stat.setString(3, "jack@xxx.com");
stat.executeUpdate();

參數化查詢 Wiki

參數化查詢(parameterized query 或 parameterized statement)是指在設計與資料庫連結並存取資料時,在需要填入數值或資料的地方,使用參數(parameter)來給值,這個方法目前已被視為最有效可預防SQL注入攻擊的攻擊手法的防禦方式。 除了安全因素,相比起拼接字串的SQL語句,參數化的查詢往往有效能優勢。因為參數化的查詢能讓不同的資料通過參數到達資料庫,從而公用同一條SQL語句。大多數資料庫會快取解釋SQL語句產生的位元組碼而省下重複解析的開銷。如果採取拼接字串的SQL語句,則會由於運算元據是SQL語句的一部分而非參數的一部分,而反覆大量解釋SQL語句產生不必要的開銷。