PreparedStatement是如何防止SQL注入的?

爲何在Java中PreparedStatement可以有效防止SQL注入?這多是每一個Java程序員思考過的問題。html

 

首先咱們來看下直觀的現象(注:須要提早打開mysql的SQL文日誌java

1. 不使用PreparedStatement的set方法設置參數(效果跟Statement類似,至關於執行靜態SQL)mysql

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方法設置參數sql

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文是把整個參數用引號包起來,並把參數中的引號做爲轉義字符,從而避免了參數也做爲條件的一部分app

 


 

接下來咱們分析下源碼(以mysql驅動實現爲例)fetch

打開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」呢?ui

回答這個問題以前須要先了解下一個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注入相關的代碼進行解讀,若是在setString先後輸出預處理語句(preparedStatement.toString()),會發現以下輸出

Before bind: com.mysql.jdbc.JDBC42PreparedStatement@b1a58a3: select file from file where name = ** NOT SPECIFIED **
After bind: com.mysql.jdbc.JDBC42PreparedStatement@b1a58a3: select file from file where name = '\'test\' or 1=1'

編程中建議你們使用PrepareStatement + Bind-variable的方式避免SQL注入

你們有什麼其它的見解,歡迎留下評論!

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

相關文章
相關標籤/搜索