#{}如何防止SQL注入的?它的底層原理是什麼?

語言: CN / TW / HK

一、MyBatis中${}和#{}的區別

1.1 ${}#{}演示

數據庫數據:

圖片

dao接口:

``` List findByUsername(String username);

List findByUsername2(String username);

```

Mapper.xml:

```

    select * from user where username like #{username}

    select * from user where username like '%${value}%'

```

執行測試代碼:

``` @Test public void findByUsername() throws Exception {     InputStream in = Resources.getResourceAsStream("SqlMapConfig.xml");

SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();

SqlSessionFactory factory = builder.build(in);

// true:自動提交     SqlSession session = factory.openSession(true);

UserDao userDao = session.getMapper(UserDao.class);

List userList = userDao.findByUsername("%小%");     List userList2 = userDao.findByUsername2("小");

System.out.println("userList: ");     for (User user : userList) {         System.out.println(user);     }

System.out.println("userList2: ");

for (User user : userList2) {         System.out.println(user);     }

session.close();     in.close(); }

```

查看執行結果:

圖片

發現都能夠查詢出來

1.2 SQL注入問題

${}會產生SQL注入,#{}不會產生SQL注入問題

我們做一個測試:

``` List userList2 = userDao.findByUsername2(" aaa' or 1=1 -- ");

System.out.println("userList2: ");

for (User user : userList2) {     System.out.println(user); }

```

查詢生成的SQL語句:

圖片

我們傳遞的參數是aaa' or 1=1 --,導致查詢出來了全部的數據。

大家可以想象一下,如果我是要根據id刪除呢?

``` delete from user where id='${value}'

```

如果我傳遞的是:1' or 1=1; --,結果會是什麼樣,我想大家應該已經知道了。

我這裏id是Integer類型,不好測試,就不帶大家測試了,大家有興趣可以自己私下測試。

如果上面使用的是#{}就不會出現SQL注入的問題了

圖片

1.3 ${}#{}的區別

#{}匹配的是一個佔位符,相當於JDBC中的一個?,會對一些敏感的字符進行過濾,編譯過後會對傳遞的值加上雙引號,因此可以防止SQL注入問題。

${}匹配的是真實傳遞的值,傳遞過後,會與sql語句進行字符串拼接。${}會與其他sql進行字符串拼接,不能預防sql注入問題。

查看#{}${}生成的SQL語句:

圖片

``` String abc=“123”;

{abc}="123"

${value}=123;

```

1.4 #{}底層是如何防止SQL注入的?

1.4.1 網上的答案

網上關於這類問題非常多,總結出來就兩個原因:

1)#{}底層採用的是PreparedStatement,會預編譯,因此不會產生SQL注入問題;

其實預編譯是MySQL自己本身的功能,和PreparedStatement沒關係;而且預編譯也不是咱們理解的那個預編譯,再者PreparedStatement底層默認根本沒有用到預編譯(要我們手動開啟)!詳細往下看

2)#{}不會產生字符串拼接,${}會產生字符串拼接,因此${}會出現SQL注入問題;

這兩個答案都經不起深究,最終答案也只是停留在表面,也沒人知道具體是為什麼。

1.4.2 為什麼能防止SQL注入?

我們翻開MySQL驅動的源碼一看究竟;

打開PreparedStatement類的setString()方法(MyBatis在#{}傳遞參數時,是藉助setString()方法來完成,${}則不是):

圖片

setString()方法全部源碼:

``` public void setString(int parameterIndex, String x) throws SQLException {         synchronized(this.checkClosed().getConnectionMutex()) {             if (x == null) {                 this.setNull(parameterIndex, 1);             } else {                 this.checkClosed();                 int stringLength = x.length();                 StringBuilder buf;                 if (this.connection.isNoBackslashEscapesSet()) {                     boolean needsHexEscape = this.isEscapeNeededForString(x, stringLength);                     Object parameterAsBytes;                     byte[] parameterAsBytes;                     if (!needsHexEscape) {                         parameterAsBytes = null;                         buf = new StringBuilder(x.length() + 2);                         buf.append('\'');                         buf.append(x);                         buf.append('\'');                         if (!this.isLoadDataQuery) {                             parameterAsBytes = StringUtils.getBytes(buf.toString(), this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor());                         } else {                             parameterAsBytes = StringUtils.getBytes(buf.toString());                         }

this.setInternal(parameterIndex, parameterAsBytes);                     } else {                         parameterAsBytes = null;                         if (!this.isLoadDataQuery) {                             parameterAsBytes = StringUtils.getBytes(x, this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor());                         } else {                             parameterAsBytes = StringUtils.getBytes(x);                         }

this.setBytes(parameterIndex, parameterAsBytes);                     }

return;                 }

String parameterAsString = x;                 boolean needsQuoted = true;                 if (this.isLoadDataQuery || this.isEscapeNeededForString(x, stringLength)) {                     needsQuoted = false;                     buf = new StringBuilder((int)((double)x.length() * 1.1D));                     buf.append('\'');

for(int i = 0; i < stringLength; ++i) {  //遍歷字符串,獲取到每個字符                         char c = x.charAt(i);                         switch(c) {                         case '\u0000':                             buf.append('\');                             buf.append('0');                             break;                         case '\n':                             buf.append('\');                             buf.append('n');                             break;                         case '\r':                             buf.append('\');                             buf.append('r');                             break;                         case '\u001a':                             buf.append('\');                             buf.append('Z');                             break;                         case '"':                             if (this.usingAnsiMode) {                                 buf.append('\');                             }

buf.append('"');                             break;                         case '\'':                             buf.append('\');                             buf.append('\'');                             break;                         case '\':                             buf.append('\');                             buf.append('\');                             break;                         case '¥':                         case '₩':                             if (this.charsetEncoder != null) {                                 CharBuffer cbuf = CharBuffer.allocate(1);                                 ByteBuffer bbuf = ByteBuffer.allocate(1);                                 cbuf.put(c);                                 cbuf.position(0);                                 this.charsetEncoder.encode(cbuf, bbuf, true);                                 if (bbuf.get(0) == 92) {                                     buf.append('\');                                 }                             }

buf.append(c);                             break;                         default:                             buf.append(c);                         }                     }

buf.append('\'');                     parameterAsString = buf.toString();                 }

buf = null;                 byte[] parameterAsBytes;                 if (!this.isLoadDataQuery) {                     if (needsQuoted) {                         parameterAsBytes = StringUtils.getBytesWrapped(parameterAsString, '\'', '\'', this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor());                     } else {                         parameterAsBytes = StringUtils.getBytes(parameterAsString, this.charConverter, this.charEncoding, this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), this.getExceptionInterceptor());                     }                 } else {                     parameterAsBytes = StringUtils.getBytes(parameterAsString);                 }

this.setInternal(parameterIndex, parameterAsBytes);                 this.parameterTypes[parameterIndex - 1 + this.getParameterIndexOffset()] = 12;             }

}     }

```

我們執行#{}的查詢語句,打斷點觀察:

圖片

最終傳遞的參數如下:

圖片

最終傳遞的參數為:'aaa\' or 1=1 --

咱們在數據庫中執行如下SQL語句(肯定是查詢不到數據的):

``` select * from user where username like 'aaa\' or 1=1 -- '

```

圖片

如果把PreparedStatement加的那根"/"去掉呢?我們執行SQL試試:

``` select * from user where username like 'aaa' or 1=1 -- '

```

圖片

我們也可以通過MySQL的日誌來觀察#{}${}產生的SQL語句來分析問題:

1)開啟MySQL日誌:

在MySQL配置文件中的[mysqld]下增加如下配置:

```

是否開啟mysql日誌  0:關閉(默認值) 1:開啟

general-log=1

mysql 日誌的存放位置

general_log_file="D:/query.log"

```

圖片

2)重啟MySQL服務(要以管理員身份運行):

圖片

``` net stop mysql

net start mysql

```

使用mybatis分別執行如下兩條SQL語句:

圖片

查看MySQL日誌:

圖片

1.5 #{}${}的應用場景

既然#{}${}好那麼多,那為什麼還要有${}這個東西存在呢?乾脆都用#{}不就萬事大吉嗎?

其實不是的,${}也有用武之地,我們都知道${}會產生字符串拼接,來生成一個新的字符串

1.5.1 ${}和#{}用法上的區別

例如現在要進行模糊查詢,查詢user表中姓張的所有員工的信息

sql語句為:select * from user where name like '張%'

此時如果傳入的參數是 “張”

如果使用${}select * from user where name like '${value}%'

生成的sql語句:select * from user where name like '張%'

如果使用#{}select * from user where name like #{value}"%"

生成的sql語句:select * from user where name like '張'"%"

如果傳入的參數是 “張%”

使用#{}select * from user where name like #{value}

生成的sql語句:select * from user where name like '張%'

使用${}select * from user where name like '${value}'

生成的sql語句:select * from user where name like '張%'

通過上面的SQL語句我們能夠發現#{}是會加上雙引號,而${}匹配的是真實的值。

還有一點就是如果使用${}的話,裏面必須要填value,即:${value}#{}則隨意

1.5.2 什麼情況下用${}

場景舉例:

圖片

代碼測試:

圖片

執行之後,發現執行成功

圖片

我們可以切換一下,把${}改成#{},會出現SQL語法錯誤的異常

圖片

1.6 總結

1.6.1 SQL注入問題

MyBatis的#{}之所以能夠預防SQL注入是因為底層使用了PreparedStatement類的setString()方法來設置參數,此方法會獲取傳遞進來的參數的每個字符,然後進行循環對比,如果發現有敏感字符(如:單引號、雙引號等),則會在前面加上一個'/'代表轉義此符號,讓其變為一個普通的字符串,不參與SQL語句的生成,達到防止SQL注入的效果。

其次${}本身設計的初衷就是為了參與SQL語句的語法生成,自然而然會導致SQL注入的問題(不會考慮字符過濾問題)。

1.6.2 #{}${}用法總結 1)#{}在使用時,會根據傳遞進來的值來選擇是否加上雙引號,因此我們傳遞參數的時候一般都是直接傳遞,不用加雙引號,${}則不會,我們需要手動加

2)在傳遞一個參數時,我們説了#{}中可以寫任意的值,${}則必須使用value;即:${value}

3)#{}針對SQL注入進行了字符過濾,${}則只是作為普通傳值,並沒有考慮到這些問題

4)#{}的應用場景是為給SQL語句的where字句傳遞條件值,${}的應用場景是為了傳遞一些需要參與SQL語句語法生成的值。