1. 程式人生 > >PreparedStatement是如何防止SQL注入的?

PreparedStatement是如何防止SQL注入的?

為什麼在Java中PreparedStatement能夠有效防止SQL注入?這可能是每個Java程式設計師思考過的問題。

首先我們來看下直觀的現象(注:需要提前開啟mysql的SQL文列印

1. 不使用PreparedStatement的set方法設定引數(效果跟Statement相似,相當於執行靜態SQL)

String param = "'test' or 1=1";
String sql = "select file from file where name = " + param; // 拼接SQL引數
PreparedStatement preparedStatement 
= connection.prepareStatement(sql); ResultSet resultSet = preparedStatement.executeQuery(); System.out.println(resultSet.next());

輸出結果為true,DB中執行的SQL為

-- 永真條件1=1成為了查詢條件的一部分,可以返回所有資料,造成了SQL注入問題select file from file where name = 'test' or 1=1 

2. 使用PreparedStatement的set方法設定引數

String param =
"'test' or 1=1"; String sql = "select file from file where name = ?"; PreparedStatement preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1, param); ResultSet resultSet = preparedStatement.executeQuery(); System.out.println(resultSet.next());

輸出結果為false,DB中執行的SQL為

select file from file where name = '\'test\' or 1=1'

我們可以看到輸出的SQL文是把整個引數用引號包起來,並把引號作為轉義字元,從而避免了引數也作為條件的一部分

接下來我們分析下原始碼(以mysql驅動實現為例)

開啟java.sql.PreparedStatement通用介面,看到如下注釋,瞭解到PreparedStatement就是為了提高statement(包括SQL,儲存過程等)執行的效率。

An object that represents a precompiled SQL statement.A SQL statement is precompiled and stored in a PreparedStatement object. This object can then be used to efficiently execute this statement multiple times.

那麼,什麼是所謂的“precompiled SQL statement”呢?

回答這個問題之前需要先了解下一個SQL文在DB中執行的具體步驟:

  1. Convert given SQL query into DB format -- 將SQL語句轉化為DB形式(語法樹結構)
  2. Check for syntax -- 檢查語法
  3. Check for semantics -- 檢查語義
  4. Prepare execution plan -- 準備執行計劃(也是優化的過程,這個步驟比較重要,關係到你SQL文的效率,準備在後續文章介紹)
  5. Set the run-time values into the query -- 設定執行時的引數
  6. Run the query and fetch the output -- 執行查詢並取得結果

而所謂的“precompiled SQL statement”,就是同樣的SQL文(包括不同引數的),1-4步驟只在第一次執行,所以大大提高了執行效率(特別時對於需要重複執行同一SQL的)

言歸正傳,回到source中,我們重點關注一下setString方法(因為其它設定引數的方法諸如setInt,setDouble之類,編譯器會檢查引數型別,已經避免了SQL注入。)

檢視mysql中實現PreparedStatement介面的類com.mysql.jdbc.PreparedStatement中的setString方法(部分程式碼)

    public void setString(int parameterIndex, String x) throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            // if the passed string is null, then set this column to null
            if (x == null) {
                setNull(parameterIndex, Types.CHAR);
            } else {
                checkClosed();

                int stringLength = x.length();

                if (this.connection.isNoBackslashEscapesSet()) {
                    // Scan for any nasty chars                    // 判斷是否需要轉義處理(比如包含引號,換行等字元)
                    boolean needsHexEscape = isEscapeNeededForString(x, stringLength);                     // 如果不需要轉義,則在兩邊加上單引號
                    if (!needsHexEscape) {
                        byte[] parameterAsBytes = null;

                        StringBuilder quotedString = new StringBuilder(x.length() + 2);
                        quotedString.append('\'');
                        quotedString.append(x);
                        quotedString.append('\'');

                        ...
                    } else {
                        ...
                }

                String parameterAsString = x;
                boolean needsQuoted = true;
                // 如果需要轉義,則做轉義處理
                if (this.isLoadDataQuery || isEscapeNeededForString(x, stringLength)) {
                ...

從上面加紅色註釋的可以明白為什麼引數會被單引號包裹,並且類似單引號之類的特殊字元會被轉義處理,就是因為這些程式碼的控制避免了SQL注入。

這裡只對SQL注入相關的程式碼進行解讀,有什麼其它的看法,歡迎大家留下評論!

參考:https://stackoverflow.com/questions/30587736/what-is-pre-compiled-sql-statement