1. 程式人生 > >wireshark抓包分析mybatis的sql引數化查詢

wireshark抓包分析mybatis的sql引數化查詢

  我們使用jdbc操作資料庫的時候,都習慣性地使用引數化的sql與資料庫互動。因為引數化的sql有兩大有點,其一,防止sql注入;其二,提高sql的執行效能(同一個connection共用一個的sql編譯結果)。下面我們就通過mybatis來分析一下引數化sql的過程,以及和非引數化sql的不同。

  注意:

    ①本次使用wireshark來監聽網絡卡的請求,測試過程中,如果使用的是本地的mysql的話,java和mysql的互動是不需要經過wireshark的,所以如果是想用wireshark監聽網絡卡的請求,推薦是連結遠端的資料庫。

    ②本文的專案原始碼在文章末尾有連結(專案原始碼中也有設計的表的sql)。

    ③可以結合wiereshark的抓包和mysql的general_log一起來檢視sql的引數化過程,文章末尾會貼上從mysql的general_log角度檢測到useServerPrepStmts=true/false兩種執行方式的區別。

  

  一開始,專案中我的db配置如下,我們就先用這個配置來測試一下。

  

jdbc:mysql://xxx.xxx.xxx.xxx:3306/test?characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai

  mapper.xml如下

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="mapper.UserMapper">
  <select id="findByName" resultType="domain.User">
  select *
  from `user`
  where user_id = #{name}
  </select>
</mapper>

  測試用例如下:

public class UserMapperTest {

    @Test
    public void findByPk() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        try (SqlSession session = sqlSessionFactory.openSession()) {
            UserMapper mapper = session.getMapper(UserMapper.class);
            User user = mapper.findByName("SYS_APP_d5bwx8AfZXkKewMkOkaZhBv7MWqjiDX3qIkfPkG8");
            System.out.println(user);
        }
    }
}

 

  執行測試用例,通過wireshak監聽請求,如下:

  從上圖wireshark抓到的資料來看,執行查詢並沒有使用preparestatement,也不是引數化的sql,都是把拼裝好引數的sql傳送到mysql執行引擎去執行,為什麼呢?經過查資料發現,db配置的url配置,漏了一個屬性配置分別是useServerPrepStmts,修改後的db的url配置如下:

jdbc:mysql://xxx.xxx.xxx.xxx:3306/test?characterEncoding=utf-8&amp;useSSL=false&amp;serverTimezone=Asia/Shanghai&amp;useServerPrepStmts=true

  增加了useServerPrepStmts屬性配置之後,再來執行測試用例,看wireshark抓到的資料如下:

  (ps.如果useServerPrepStmts=true,是通過wireshark抓包結果可以看到,先是傳送Request Prepare Statement--sql模板(同一connection第一次執行改sql模板才會傳送,後面就不會在傳送該Request),再發送Request Execute Statement--sql引數。而useServerPrepStmts=false的話,都是清一色的Request Query,其實就是沒用到mysql server的預編譯功能,所以是推薦配置useServerPrepStmts=true,提高參數化sql的執行效能)

  上圖就是先發送待執行的sql模板(不帶引數)到mysql服務端進行預編譯,並且會在該請求的response中返回該sql編譯之後的id,名曰:Statement ID,wireshark抓到的response資料如下:

  

 

  傳送完sql模板之後,從response中拿到statement id之後,緊跟著就傳送引數和statement id到mysql執行引擎,wireshark抓到的資料如下:

 

  如此,便可實現sql的引數化查詢。按照理解,如果此時再用此sql模板查詢另外一個user_id的資料,理論上是不需要再發送sql模板到mysql伺服器了的,只需要傳送引數和statement ID就可以了的,下面我就試一下,測試用例如下:

    @Test
    public void findByPk() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        try (SqlSession session = sqlSessionFactory.openSession()) {
            UserMapper mapper = session.getMapper(UserMapper.class);
            User user = mapper.findByName("SYS_APP_d5bwx8AfZXkKewMkOkaZhBv7MWqjiDX3qIkfPkG8");
            if (user != null || user == null) {
                user = mapper.findByName("SYS_APP_d5bwx8AfZXkKewMkOkaZhBv7MWqjiDX3qIkfPkG8");
            }
            System.out.println(user);
        }
    }

  執行測試用例,用wireshark抓包,如下:

  通過上圖發現,怎麼第二個findByName,壓根就沒發請求到mysql伺服器,原來是因為本地的jdbc發現是相同的查詢,直接返回了上一個查詢的結果,所以不需要重新到mysql伺服器去請求資料。那我在第二個findByName改一個和第一個不一樣的引數,測試用例如下:

    @Test
    public void findByPk() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        try (SqlSession session = sqlSessionFactory.openSession()) {
            UserMapper mapper = session.getMapper(UserMapper.class);
            User user = mapper.findByName("SYS_APP_d5bwx8AfZXkKewMkOkaZhBv7MWqjiDX3qIkfPkG8");
            if (user != null || user == null) {
                user = mapper.findByName("SYS_APP_d5bwx8AfZXkKewMkOkaZhBv7MWqjiDX3qIkfPkG9");
            }
            System.out.println(user);
        }
    }

  執行上面這個測試用例,wireshark抓包結果如下:

  上圖發現,竟然兩次請求都重複傳送了模板sql到mysql伺服器預編譯,為何呢?原來db的url配置裡面還漏了一個屬性配置(cachePrepStmts),增加cachePrepStmts配置之後,db的url配置如下:

jdbc:mysql://xxx.xxx.xxx.xxx:3306/test?characterEncoding=utf-8&amp;useSSL=false&amp;serverTimezone=Asia/Shanghai&amp;useServerPrepStmts=true&amp;cachePrepStmts=true

  更新db的url配置之後,再執行測試用例,wireshark抓包結果如下:

 

   通過上圖發現,第二次findByName不再發送模板sql了,直接就是傳送Execute Statement了,其實Execute Statement就是執行sql的引數和statement ID(該connection第一次預編譯模板sq的時候l返回的)。但是中間還是會發一個Reset Statement的mysql資料包,為什麼要發這個Reset Statement資料包,有知道的同學,可以評論回覆一下,我也還沒去深究~謝謝~

   這裡附帶再說一下mybatis的引數化sql可以防止sql注入的理解,其實防止sql注入,有兩點,其一,mybatis本身會有一個sql引數化的過程,這裡涉及到mybatis的#和$的區別,引數化sql是用#引用變數,mybatis會對引數進行特殊字元以及敏感字元的轉義以防止sql注入;其二,db的url配置中加了useServerPrepStmts=true之後,mysql服務端會對Execute Statement傳送的引數中涉及的敏感字元進行轉義,以防止sql注入,所以,如果不加useServerPrepStmts=true的話,會發現,mybatis在本地就已經對引數中涉及的敏感字元進行了轉義之後,再發往mysql server,可以使用wireshark抓包看到;但是如果是加了useServerPrepStmts=true之後,會發現client發往mysql server的引數(Execute Statement),mybatis不會對其中的引數進行轉義了,引數敏感字元轉義這一塊交給了mysql server去做,也可以通過wireshark抓包看到。so,這裡會有兩塊地方防止sql注入,一塊在client,一塊在mysql server(使用儲存過程防止sql注入也是使用了mysql server的該功能),就看你是否使用useServerPrepStmts。

 

附錄:

 1.useServerPrepStmts=false/true,wireshark抓包結果

  useServerPrepStmts=false,wireshark抓包結果如下:

  

  useServerPrepStmts=true,wireshark抓包結果如下:

  

 

2. mysql server的general_log角度檢測到useServerPrepStmts=false/true的執行sql

  useServerPrepStmts=false的general_log

  

2019-08-18T15:19:12.330744Z       38 Query    SET autocommit=0
2019-08-18T15:19:12.345704Z       38 Query    select *
        from `user`
        where user_id = 'SYS_APP_d5bwx8AfZXkKewMkOkaZhBv7MWqjiDX3qIkfPkG8\' or 1 = 1 #'
2019-08-18T15:19:12.358669Z       38 Query    select *
        from `user`
        where user_id = 'SYS_APP_d5bwx8AfZXkKewMkOkaZhBv7MWqjiDX3qIkfPkG9'
2019-08-18T15:19:12.359666Z       38 Query    SET autocommit=1

 

  useServerPrepStmts=true的general_log  

 

2019-08-18T09:39:42.533289Z       30 Query    SET autocommit=0
2019-08-18T09:39:42.546254Z       30 Prepare    select *
        from `user`
        where user_id = ?
2019-08-18T09:39:42.550244Z       30 Execute    select *
        from `user`
        where user_id = 'SYS_APP_d5bwx8AfZXkKewMkOkaZhBv7MWqjiDX3qIkfPkG8\' or 1 = 1 #'
2019-08-18T09:39:42.560217Z       30 Reset stmt    
2019-08-18T09:39:42.561214Z       30 Execute    select *
        from `user`
        where user_id = 'SYS_APP_d5bwx8AfZXkKewMkOkaZhBv7MWqjiDX3qIkfPkG9'
2019-08-18T09:39:42.563210Z       30 Query    SET autocommit=1

&n