摘要:我們對語句做適當改變,就完成了注入,因為普通的不會對做任何處理,該例中單引號后的生效,拉出了所有數據。查詢資料后,發現還要開啟一個參數,讓端緩存,緩存是級別的。結論是個好東西。
背景
最近因為工作調整的關系,都在和數據庫打交道,增加了許多和JDBC親密接觸的機會,其實我們用的是Mybatis啦。知其然,知其所以然,是我們工程師童鞋們應該追求的事情,能夠幫助你更好的理解這個技術,面對問題時更游刃有余。所以呢,最近就在業務時間對JDBC進行了小小的研究,有一些小收獲,在此做個記錄。
我們都知道市面上有很多數據庫,比如Oracle,Sqlserver以及Mysql等,因為Mysql開放性以及可定制性比較強,平時在學校里或者在互聯網從業的開發人員應該接觸Mysql最多,本文后續的講解也主要針對的是JDBC在Mysql驅動中的相關實現。
提綱本文簡單介紹了JDBC的由來,介紹了JDBC使用過程中的驅動加載代碼,介紹了幾個常用的接口,著重分析了Statement和Preparement使用上以及他們對待SQL注入上的區別。最后著重分析了PrepareStatement開啟預編譯前后,防SQL注入以及具體執行上的區別。
為什么需要JDBC我們都知道,每家數據庫的具體實現都會有所不同,如果開發者每接觸一種新的數據庫,都需要對其具體實現進行編程了,那我估計真正的代碼還沒開始寫,先累死在底層的開發上了,同時這也不符合Java面向接口編程的特點。于是就有了JDBC。
JDBC(Java Data Base Connectivity,java數據庫連接)是一種用于執行SQL語句的Java API,可以為多種關系數據庫提供統一訪問,它由一組用Java語言編寫的類和接口組成。
如果用圖來表示的話,如上圖所示,開發者不必為每家數據通信協議的不同而疲于奔命,只需要面向JDBC提供的接口編程,在運行時,由對應的驅動程序操作對應的DB。
光說不練假把式,奉上一段簡單的示例代碼,主要完成了獲取數據庫連接,執行SQL語句,打印返回結果,釋放連接的過程。
package jdbc; import java.sql.*; /** * @author cenkailun * @Date 17/5/20 * @Time 下午5:09 */ public class Main { private static final String url = "jdbc:mysql://127.0.0.1:3306/demo"; private static final String user = "root"; private static final String password = "123456"; static { try { Class.forName("com.mysql.jdbc.Driver"); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) throws SQLException { Connection connection = DriverManager.getConnection(url, user, password); System.out.println("Statement 語句結果: "); Statement statement = connection.createStatement(); statement.execute("SELECT * FROM SU_City limit 3"); ResultSet resultSet = statement.getResultSet(); printResultSet(resultSet); resultSet.close(); statement.close(); System.out.println(); System.out.println("PreparedStatement 語句結果: "); PreparedStatement preparedStatement = connection .prepareStatement("SELECT * FROM SU_City WHERE city_en_name = ? limit 3"); preparedStatement.setString(1, "beijing"); preparedStatement.execute(); resultSet = preparedStatement.getResultSet(); printResultSet(resultSet); resultSet.close(); preparedStatement.close(); connection.close(); } /** * 處理返回結果集 */ private static void printResultSet(ResultSet rs) { try { ResultSetMetaData meta = rs.getMetaData(); int cols = meta.getColumnCount(); StringBuffer b = new StringBuffer(); while (rs.next()) { for (int i = 1; i <= cols; i++) { b.append(meta.getColumnName(i) + "="); b.append(rs.getString(i) + " "); } b.append(" "); } System.out.print(b.toString()); } catch (Exception e) { e.printStackTrace(); } } }主要接口:
DriverManager: 管理驅動程序,主要用于調用驅動從數據庫獲取連接。
Connection: 代表了一個數據庫連接。
Statement: 持有Sql語句,執行并返回執行后的結果。
ResulSet: Sql執行完畢,返回的記過持有
代碼分析接下來我們對示例代碼進行分析,闡述相關的知識點,具體實現均針對
驅動加載mysql mysql-connector-java 5.1.42
在示例代碼的static代碼塊,我們執行了
Class.forName("com.mysql.jdbc.Driver");
Class.forName會通過反射,初始化一個類。在com.mysql.jdbc.Driver,目測來說這是mysql對于JDBC中Driver接口的一個具體實現,在這個類里面,在其static代碼塊,它向DriverManager注冊了自己。
public class Driver extends NonRegisteringDriver implements java.sql.Driver { // // Register ourselves with the DriverManager // static { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can"t register driver!"); } } /** * Construct a new driver and register it with DriverManager * * @throws SQLException * if a database error occurs. */ public Driver() throws SQLException { // Required for Class.forName().newInstance() } }
在DriverManger有一個CopyOnWriterArrayList,保存了注冊驅動,以后可以再介紹一下它,它是在寫的時候復制一份出去寫,寫完再復制回去。
private final static CopyOnWriteArrayListregisteredDrivers = new CopyOnWriteArrayList ();
注冊完驅動后,我們可以通過DriverManager拿到Connection,這里有一個疑問,如果注冊了多個驅動怎么辦? JDBC對這種也有應對方法,在選擇使用哪個驅動的時候,會調用每個驅動實現的acceptsURL,判斷這個驅動是不是符合條件。
public static Driver getDriver(String url) throws SQLException { Class> callerClass = Reflection.getCallerClass(); for (DriverInfo aDriver : registeredDrivers) { if(isDriverAllowed(aDriver.driver, callerClass)) { try { if(aDriver.driver.acceptsURL(url)) { return (aDriver.driver); } ..............................................
如果有多個符合條件的驅動,就先到先得唄~
接下來是構建Sql語句。statement有三個具體的實現類:
PreparedStatement: PreparedStatement創建時就傳過去一個sql語句,開始預編譯的話,會返回語句ID,下次傳語句ID和參數過去,就少了一次編譯過程。
Statement: Statement用Connection得到一個空的執行器,在執行的時候給它傳拼好的死的sql ,因為是整一個SQL,所以完全匹配的概率低,每次都需要重新解析編譯。
CallableStatement 用于執行存儲過程,目前沒遇到過。
下文主要講Statement和PreparedStatement。
前提:mysql執行腳本的大致過程如下:prepare(準備)-> optimize(優化)-> exec(物理執行),其中,prepare也就是我們所說的編譯。前面已經說過,對于同一個sql模板,如果能將prepare的結果緩存,以后如果再執行相同模板而參數不同的sql,就可以節省掉prepare(準備)的環節,從而節省sql執行的成本
StatementStatement可以理解為,每次都會把SQL語句,完整傳輸到Mysql端,被人一直詬病的,就是其難以防止最簡單的Sql注入。
2017-05-20T10:07:20.439856Z 15 Query SET NAMES latin1 2017-05-20T10:07:20.440138Z 15 Query SET character_set_results = NULL 2017-05-20T10:07:20.440733Z 15 Query SET autocommit=1 2017-05-20T10:07:20.445518Z 15 Query SELECT * FROM SU_City limit 3
我們對statement語句做適當改變,city_en_name = ""beijing" OR 1 = 1",就完成了SQL注入,因為普通的statement不會對SQL做任何處理,該例中單引號后的OR 生效,拉出了所有數據。
2017-05-20T10:10:02.739761Z 17 Query SELECT * FROM SU_City WHERE city_en_name = "beijing" OR 1 = 1 limit 3PreparedStatement
對于PreparedStatement,之前的認識是因為使用了這個,它會預編譯,所以能防止SQL注入,所以為什么它能防止呢,說不清楚。我們先來看一下效果。
2017-05-20T10:14:16.841835Z 19 Query SELECT * FROM SU_City WHERE city_en_name = ""beijing" OR 1 = 1 " limit 3
同樣的代碼,單引號被轉義了,所以沒被SQL注入。
但我希望大家注意到,在這里,我們并沒有開啟預編譯哦。所以說因為開啟預編譯,能防止SQL注入是不對的。
圍觀了下代碼,發現在未開啟預編譯的時候,在setString時,使用的是mysql驅動的PreparedStatement,在這個方法里,會對參數進行處理。
publicvoidsetString(intparameterIndex, String x)throwsSQLException {
大致是在這里。
for (int i = 0; i < stringLength; ++i) { char c = x.charAt(i); switch (c) { case 0: /* Must be escaped for "mysql" */ buf.append(""); buf.append("0"); break; case " ": /* Must be escaped for logs */ buf.append(""); buf.append("n"); break; case " ": buf.append(""); buf.append("r"); break; case "": buf.append(""); buf.append(""); break; case """: buf.append(""); buf.append("""); break;
所以因為開啟預編譯才防止SQL注入是不對的,當然開啟預編譯后,確實也能防止。
Mysql其實是支持預編譯的。你需要在JDBCURL里指定,這樣就開啟預編譯成功。
"jdbc:mysql://127.0.0.1:3306/demo?useServerPrepStmts=true"
同時我們可以證明開啟服務端預編譯后,參數是在Mysql端進行轉義了。下文是開啟服務端預編譯后,具體的日志情況。開啟wireshark,可以看到傳參數時是沒有轉義的,所以在服務端Mysql也能夠對個別字符進行轉義處理。
2017-05-20T10:27:53.618269Z 20 Prepare SELECT * FROM SU_City WHERE city_en_name = ? limit 3 2017-05-20T10:27:53.619532Z 20 Execute SELECT * FROM SU_City WHERE city_en_name = ""beijing" OR 1 = 1 " limit 3
再深入一點,如果是新開啟一個PrepareStatement,會看到,還是要預編譯兩次,那預編譯的意義就沒有了,等于每次都多了一次網絡傳輸。
2017-05-20T10:33:26.206977Z 23 Prepare SELECT * FROM SU_City WHERE city_en_name = ? limit 3 2017-05-20T10:33:26.208019Z 23 Execute SELECT * FROM SU_City WHERE city_en_name = ""beijing" OR 1 = 1 " limit 3 2017-05-20T10:33:26.208829Z 23 Prepare SELECT * FROM SU_City WHERE city_en_name = ? limit 3 2017-05-20T10:33:26.209098Z 23 Execute SELECT * FROM SU_City WHERE city_en_name = ""beijing" OR 1 = 1 " limit 3
查詢資料后,發現還要開啟一個參數,讓JVM端緩存,緩存是Connection級別的。然后看效果。
"jdbc:mysql://127.0.0.1:3306/demo?useServerPrepStmts=true&cachePrepStmts=true";
查看日志,發現還是兩次,?我了。
2017-05-20T10:34:51.540301Z 25 Prepare SELECT * FROM SU_City WHERE city_en_name = ? limit 3 2017-05-20T10:34:51.541307Z 25 Execute SELECT * FROM SU_City WHERE city_en_name = ""beijing" OR 1 = 1 " limit 3 2017-05-20T10:34:51.542025Z 25 Prepare SELECT * FROM SU_City WHERE city_en_name = ? limit 3 2017-05-20T10:34:51.542278Z 25 Execute SELECT * FROM SU_City WHERE city_en_name = ""beijing" OR 1 = 1 " limit 3
陰差陽錯,點進PrepareStatement的close方法,才看到如下代碼,恍然大悟,一定要關閉,緩存才會生效。
public void close() throws SQLException { MySQLConnection locallyScopedConn = this.connection; if (locallyScopedConn == null) { return; // already closed } synchronized (locallyScopedConn.getConnectionMutex()) { if (this.isCached && isPoolable() && !this.isClosed) { clearParameters(); this.isClosed = true; this.connection.recachePreparedStatement(this); return; } realClose(true, true); } }
其實是假裝關閉了statement,其實是把statement塞進緩存了。然后我們再看看效果,完美。
2017-05-20T10:39:39.410584Z 26 Prepare SELECT * FROM SU_City WHERE city_en_name = ? limit 3 2017-05-20T10:39:39.411715Z 26 Execute SELECT * FROM SU_City WHERE city_en_name = ""beijing" OR 1 = 1 " limit 3 2017-05-20T10:39:39.412388Z 26 Execute SELECT * FROM SU_City WHERE city_en_name = ""beijing" OR 1 = 1 " limit 3結論
JDBC是個好東西。
Statement沒有防止SQL注入的能力。
PrepareStatement在沒有開啟預編譯時,在本地對SQL進行參數化處理,對個別字符進行轉移,開啟預編譯時,交由mysql端進行轉移處理。
建議都使用PrepareStatement,因為其在本地也可以進行防SQL注入的簡單處理,傳輸時和statement一樣傳輸一條完整的sql。
如果開啟PrepareStatement的useServerPrepStmts=true特性,請同時開啟cachePrepStmts=true,否則同樣的SQL模板,每次要進行一次編譯,一次執行,網絡開銷成倍了,影響效率。
想進一步了解更多,可以關注我的微信公眾號
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/67418.html
摘要:在動態解析階段,和會有不同的表現解析為一個預編譯語句的參數標記符。其次,在預編譯之前已經被變量替換了,這會存在注入問題。預編譯語句對象可以重復利用。默認情況下,將對所有的進行預編譯。總結本文主要深入探究了對和的不同處理方式,并了解了預編譯。 mybatis 中使用 sqlMap 進行 sql 查詢時,經常需要動態傳遞參數,例如我們需要根據用戶的姓名來篩選用戶時,sql 如下: sele...
摘要:原因就是傳入的和原有的單引號,正好組成了,而后面恒等于,所以等于對這個庫執行了查所有的操作。類比的執行流程和原有的我們使用的方法就是。可以理解為就是用來解析定制的符號的語句。后續的流程,就和正常的流程一致了。 前言 在JDBC中,主要使用的是兩種語句,一種是支持參數化和預編譯的PrepareStatement,能夠支持原生的Sql,也支持設置占位符的方式,參數化輸入的參數,防止Sql注...
閱讀 2562·2021-11-22 09:34
閱讀 3539·2021-11-15 11:37
閱讀 2341·2021-09-13 10:37
閱讀 2105·2021-09-04 16:40
閱讀 1564·2021-09-02 15:40
閱讀 2456·2019-08-30 13:14
閱讀 3326·2019-08-29 13:42
閱讀 1903·2019-08-29 13:02