JDBC程式設計之預編譯SQL與防注入式攻擊以及PreparedStatement
在JDBC程式設計中,常用Statement、PreparedStatement 和 CallableStatement三種方式來執行查詢語句,其中 Statement 用於通用查詢, PreparedStatement 用於執行引數化查詢,而 CallableStatement則是用於儲存過程。
1、Statement 該物件用於執行靜態的 SQL 語句,並且返回執行結果。 此處的SQL語句必須是完整的,有明確的資料指示。查的是哪條記錄?改的是哪條記錄?都要指示清楚。 通過呼叫 Connection 物件的 createStatement 方法建立該物件 查詢:ResultSet excuteQuery(String sql)——返回查詢結果的封裝物件ResultSet. 用next()遍歷結果集,getXX()獲取記錄資料。 修改、刪除、增加:int excuteUpdate(String sql)——返回影響的資料表記錄數.
2、PreparedStatement SQL 語句被預編譯並存儲在 PreparedStatement 物件中。然後可以使用此物件多次高效地執行該語句。 可以通過呼叫 Connection 物件的 preparedStatement() 方法獲取 PreparedStatement 物件 PreparedStatement 物件所執行的 SQL 語句中,引數用問號(?)來表示,呼叫 PreparedStatement 物件的 setXXX() 方法來設定這些引數. setXXX() 方法有兩個引數,第一個引數是要設定的 SQL 語句中的引數的索引(從 1 開始),第二個是設定的 SQL 語句中的引數的值,注意用setXXX方式設定時,需要與資料庫中的欄位型別對應,例如mysql中欄位為varchar,就需要使用setString方法,如果為Date型別,就需要使用setDate方法來設定具體sql的引數。
簡單來說就是,預編譯的SQL語句不是有具體數值的語句,而是用(?)來代替具體資料,然後在執行的時候再呼叫setXX()方法把具體的資料傳入。同時,這個語句只在第一次執行的時候編譯一次,然後儲存在快取中。之後執行時,只需從快取中抽取編譯過了的程式碼以及新傳進來的具體資料,即可獲得完整的sql命令。這樣一來就省下了後面每次執行時語句的編譯時間。
使用預編譯分4步走:
1:定義預編譯的sql語句,其中待填入的引數用 ? 佔位。注意,?無關型別,不需要加分號之類。其具體資料型別在下面setXX()時決定。
2:建立預編譯Statement,並把sql語句傳入。此時sql語句已與此preparedStatement繫結。所以第4步執行語句時無需再把sql語句作為引數傳入execute()。
3:填入具體引數。通過setXX(問號下標,數值)來為sql語句填入具體資料。注意:問號下標從1開始,setXX與數值型別有關,字串就是setString(index,str).
4:執行預處理物件。主要有:
boolean |
在此 PreparedStatement 物件中執行 SQL 語句,該語句可以是任何種類的 SQL 語句。 |
在此 PreparedStatement 物件中執行 SQL 查詢,並返回該查詢生成的 ResultSet 物件。 |
|
int |
在此 PreparedStatement 物件中執行 SQL 語句,該語句必須是一個 SQL 資料操作語言(Data Manipulation Language,DML)語句,比如 INSERT 、UPDATE 或 DELETE 語句;或者是無返回內容的 SQL 語句,比如 DDL 語句。 |
注意,前面建立preparedstatement時已經把sql語句傳入了,此時執行不需再把sql語句傳入,這是與一般statement執行sql語句所不同之處。
比如:
/**
* 由於每次addNewStudent()都要寫sql語句,寫起來比較繁瑣,所以我用PreparedStatement()進行優化
* 1. PreparedStatement:是Statement的子介面,可以傳入帶佔位符的SQL語句,並且提供了補充佔位符變數的方法
* 2. 呼叫PreparedStatement的setXxx(int index, Object val)設定佔位符從1開始,val表示要插入的資料
* 3.使用Statement需要拼寫SQL語句,很辛苦,容易出錯
* 4. 可以有效的防止SQL注入
*/
@Test
public void testPreparedStatement() {
Connection connection = null;
PreparedStatement preparedStatement = null;
try {
connection = JDBCTools.getConnection();
String sql = "INSERT INTO customers (id, name, email, birth) VALUES(?, ?, ?, ?)";
preparedStatement = (PreparedStatement) connection.prepareStatement(sql);
preparedStatement.setString(1, "2018");//1--對應id
preparedStatement.setString(2, "ATGUIGU");//2--對應name
preparedStatement.setString(3, "[email protected]");//3--對應email
preparedStatement.setDate(4, new java.sql.Date(new java.util.Date().getTime()));//4--對應birth
//執行executeUpdate()或者executeQuery()不需要在傳入sql
preparedStatement.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCTools.release(null, preparedStatement, connection);
}
}
使用預編譯的好處:
1:PreparedStatement比 Statement 更快 使用 PreparedStatement 最重要的一點好處是它擁有更佳的效能優勢,SQL語句會預編譯在資料庫系統中。執行計劃同樣會被快取起來,它允許資料庫做引數化查詢。使用預處理語句比普通的查詢更快,因為它做的工作更少(資料庫對SQL語句的分析,編譯,優化已經在第一次查詢前完成了)。
2:PreparedStatement可以防止SQL注入式攻擊
SQL 注入攻擊:SQL 注入是利用某些系統沒有對使用者輸入的資料進行充分的檢查,而在使用者輸入資料中注入非法的 SQL 語句段或命令,從而利用系統的 SQL 引擎完成惡意行為的做法。
比如:某個網站的登入驗證SQL查詢程式碼為:
sql = "SELECT * FROM users WHERE username = '" + username + "' AND " +"password = '" + password + "'";
惡意填入:
String username = "a' OR PASSWORD = ";
String password = "OR '1' = '1";
那麼最終SQL語句變成了:
sql = "SELECT * FROM users WHERE username = '" + a' OR PASSWORD = + "' AND " +"password = '" +OR '1' = '1 + "'";
因為WHERE條件恆為真,這就相當於執行:
sql = "SELECT * FROM users;"
因此可以達到無賬號密碼亦可登入網站。
如果惡意使用者要是更壞一點,SQL語句變成:
sql = "SELECT * FROM users WHERE username = 'any_value' and password = ''; DROP TABLE users"
使用PreparedStatement的引數化的查詢可以阻止大部分的SQL注入。在使用引數化查詢的情況下,資料庫系統不會將引數的內容視為SQL指令的一部分來處理,而是在資料庫完成SQL指令的編譯後,才套用引數執行,因此就算引數中含有破壞性的指令,也不會被資料庫所執行。因為對於引數化查詢來說,查詢SQL語句的格式是已經規定好了的,需要查的資料也設定好了,缺的只是具體的那幾個資料而已。所以使用者能提供的只是資料,而且只能按需提供,無法更進一步做出影響資料庫的其他舉動來。這樣一來,雖然沒有登入,但是資料表都被刪除了。S
Statement方法的完整程式碼:(容易SQL注入,不太好,不推薦)
/**
* Statement方法容易被修改sql語句造成不必要的麻煩
* SQL注入
*/
@Test
public void testSQLInjection() {
String username = "a' OR PASSWORD = ";
String password = "OR '1' = '1";
String sql = "SELECT * FROM users WHERE username = '"
+ username + "' AND " +"password = '" + password + "'";
Connection connection = null;
java.sql.Statement statement = null;
ResultSet resultSet = null;
try {
connection = JDBCTools.getConnection();
statement = connection.createStatement();
resultSet = statement.executeQuery(sql);
if(resultSet.next()) {
System.err.println("登入成功!");
} else {
System.out.println("使用者名稱或者密碼不正確!");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCTools.release(resultSet, statement, connection);
}
}
PreparedStatement方法的完整程式碼:(較好推薦用)
/**
* PreparedStatement方法就不會產生上面的麻煩
*/
@Test
public void testSQLInjection2() {
String username = "Tom";
String password = "123456";
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
Connection connection = null;
java.sql.PreparedStatement preparedstatement = null;
ResultSet resultSet = null;
try {
connection = JDBCTools.getConnection();
preparedstatement = connection.prepareStatement(sql);
preparedstatement.setString(1, username);
preparedstatement.setString(2, password);
resultSet = preparedstatement.executeQuery();
if(resultSet.next()) {
System.err.println("登入成功!");
} else {
System.out.println("使用者名稱或者密碼不正確!");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
JDBCTools.release(resultSet, preparedstatement, connection);
}
}