1. 程式人生 > >JDBC程式設計之預編譯SQL與防注入式攻擊以及PreparedStatement

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)語句,比如 INSERTUPDATE 或 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);
		}
	}