1. 程式人生 > >【Hibernate實戰】原始碼解析Hibernate引數繫結及PreparedStatement防SQL注入原理

【Hibernate實戰】原始碼解析Hibernate引數繫結及PreparedStatement防SQL注入原理

    本文采用mysql驅動是5.1.38版本。

    本篇文章涉及內容比較多,單就Hibernate來講就很大,再加上資料庫驅動和資料庫相關,非一篇文章或一篇專題就能說得完。本文從使用入手在【Spring實戰】----Spring4.3.2整合Hibernate5.2.5 基礎上繼續深入研究。本文包含以下內容:SQL語句在資料庫中的執行過程、JDBC、PreparedStatement、Hibernate引數繫結

1、SQL語句在資料庫中的執行過程(大概)

    在一般關係型資料庫系統架構下(如:Oracle、MySQL等),SQL語句由使用者程序產生,然後傳到相對應的服務端程序,之後由伺服器程序執行該SQL語句。伺服器程序處理SQL語句的基本階段是:解析、引數繫結、執行、返回結果。

1)解析

伺服器程序接收到一個SQL語句時,首先要將其轉換成執行這個SQL語句的最有效步驟,這些步驟被稱為執行計劃。

Step 1:檢查共享池中是否有之前解析相同的SQL語句後所儲存的SQL文字、解析樹和執行計劃。如果能從共享池的快取庫中找到之前解析過生成的執行計劃,則SQL語句則不需要再次解析,便可以直接由庫快取得到之前所產生的執行計劃,從而直接跳到繫結或執行階段,這種解析稱作軟解析。

但是如果在共享池的庫快取中找不到對應的執行計劃,則必須繼續解析SQL、生成執行計劃,這種解析稱作硬解析

Step 2:語法分析,分析SQL語句的語法是否符合規範,衡量語句中各表示式的意義

Step 3:檢查是否存在語義錯誤和許可權。語義分析,檢查語句中設計的所有資料庫物件是否存在,且使用者有相應的許可權。

Step 4:檢視轉換和表示式轉換 將涉及檢視的查詢語句轉換為相應的對基表查詢語句。將複雜表示式轉化較為簡單的等效連線表示式。

Step 5:決定最佳執行計劃。優化器會生成多個執行計劃,在按統計資訊帶入,找出執行成本最小的執行計劃,作為執行此SQL語句的執行計劃

Step 6:將SQL文字、解析樹、執行計劃快取到庫快取,存放地址以及SQL語句的雜湊值。

2)引數繫結

如果SQL語句中使用了繫結變數,掃描繫結變數的宣告,給繫結變數賦值。則此時將變數值帶入執行計劃。

3)執行

此階段按照執行計劃執行SQL,產生執行結果。不同型別的SQL語句,執行過程也不同。

SELECT查詢

檢查所需的資料塊是否已經在緩衝區快取中,如果已經在緩衝區快取中,直接讀取器內容即可。這種讀取方式稱為邏輯讀取。如果所需資料不在緩衝區快取中,則伺服器程序需要先掃描資料塊,讀取相應資料塊到緩衝區快取,這種讀取方式稱為物理讀。和邏輯讀相比較,它更加耗費CPU和IO資源。

修改操作(INSERT、UPDATE、DELETE)

將需要修改或刪除的行鎖住,以便在事務結束之前相同的行不會被其他程序修改。

4)返回結果

對於select語句,在執行階段,要將查詢到的結果(或被標示的行)返回給使用者程序。加入查詢結果需要排序,還要利用共享池的排序區,甚至臨時表空間的臨時段來排序。查詢結果總是以列表格式顯示。根據查詢結果的大小不同,可以一次全部返回,也可以分多次逐步返回。對於其他DML語句,將執行是否成功等狀態細心返回給使用者程序。

對於select語句,在執行階段,要將查詢到的結果(或被標示的行)返回給使用者程序。加入查詢結果需要排序,還要利用共享池的排序區,甚至臨時表空間的臨時段來排序。查詢結果總是以列表格式顯示。根據查詢結果的大小不同,可以一次全部返回,也可以分多次逐步返回。對於其他DML語句,將執行是否成功等狀態細心返回給使用者程序。

2、JDBC

怎麼用Java程式連線資料庫,並執行sql操作。JDBC粉墨登場,JDBC是Java資料庫連線(Java Database Connectivity)的縮寫,而現在是指一種用於執行SQL語句的Java API,可以為多種關係資料庫提供統一訪問,它由一組用Java語言編寫的類和介面組成,其具體實現由各資料庫驅動實現。JDBC提供了一種基準,據此可以構建更高階的工具和介面,使資料庫開發人員能夠編寫資料庫應用程式。其典型應用如下:

public static void JDBCExample(){
		try {
			Class.forName("com.mysql.jdbc.Driver");
			Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/hhl?useServerPrepStmts=true&cachePrepStmts=true&prepStmtCacheSize=25&prepStmtCacheSqlLimit=2048&characterEncoding=utf8&useSSL=false",
					"root", "123456");
			Statement statement = connection.createStatement();
			ResultSet resultSet = statement.executeQuery("SELECT p.productId FROM product p WHERE p.productName='Mango'");
			while (resultSet.next()){
				System.out.println(resultSet.getString(1));
			}
			resultSet.close();
			statement.close();
			connection.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

可見,Java訪問資料庫,是要依賴資料庫驅動的,這裡是mysql Driver。JDBC只是提供瞭如Connettion、Statement、ResultSet等介面,其具體實現是由mysql Driver實現的。其DriverManager.getConnection()也是最終呼叫的driver的connect()。如下;java.sql.DriverManager.java

 //  Worker method called by the public getConnection() methods.
    private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        /*
         * When callerCl is null, we should check the application's
         * (which is invoking this class indirectly)
         * classloader, so that the JDBC driver class outside rt.jar
         * can be loaded from here.
         */
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            // synchronize loading of the correct classloader.
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }

        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }

        println("DriverManager.getConnection(\"" + url + "\")");

        // Walk through the loaded registeredDrivers attempting to make a connection.
        // Remember the first exception that gets raised so we can reraise it.
        SQLException reason = null;

        for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    Connection con = aDriver.driver.connect(url, info);                    //最終connect
                    if (con != null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }

            } else {
                println("    skipping: " + aDriver.getClass().getName());
            }

        }

        // if we got here nobody could connect.
        if (reason != null)    {
            println("getConnection failed: " + reason);
            throw reason;
        }

        println("getConnection: no suitable driver found for "+ url);
        throw new SQLException("No suitable driver found for "+ url, "08001");
    }

那麼這裡的driver什麼時候註冊的呢,就是在執行Class.forName("com.mysql.jdbc.Driver");時(從JDBC4開始支援spi,不用顯式呼叫Class.forName(""),直接丟啊哦用DriverManager.getConnection(url)即可,載入DriverManager時會執行靜態程式碼塊,載入驅動並註冊),

java.sql.DriverManager.java

 /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

載入Driver時,會向DriverManager註冊Driver,com.mysql.jdbc.Driver.java

// Register ourselves with the DriverManager
    //
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

儘管JDBC將介面統一化了,但是如果用JDBC操作資料庫,還是需要寫sql語句,sql語句針對不同的資料庫也會不同,為了更好地實現跨資料庫操作,於是誕生了Hibernate專案,Hibernate是對JDBC的再封裝,實現了對資料庫操作更寬泛的統一和更好的可移植性。因此Hibernate也是建立在JDBC的基礎上的,JDBC又是通過資料庫driver操作資料庫的。

為了更好地實現跨資料庫操作,於是誕生了Hibernate專案,Hibernate是對JDBC的再封裝,實現了對資料庫操作更寬泛的統一和更好的可移植性。

3、PreparedStatement

PreparedStatement也是JDBC提供的介面,其實現也是在driver中的,典型用法如下:

PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM product p WHERE p.productName=?");
			preparedStatement.setString(1,"Mango");
			ResultSet resultSet = preparedStatement.executeQuery();
			String productId;
			while (resultSet.next()){
				productId = (String) resultSet.getObject(1);
				System.out.println(productId);
			}
			resultSet.close();
			preparedStatement.close();
			connection.close();


通過呼叫connection.preparedStatement(sql)方法可以獲得PreparedStatment物件。資料庫系統會對sql語句進行預編譯處理(如果JDBC驅動支援的話),預處理語句將被預先編譯好,這條預編譯的sql查詢語句能在將來的操作中重用,這樣一來,它比Statement物件生成的查詢速度更快。這裡所說的預編譯就是1中的sql執行過程中的解析。

In database management systems, a prepared statement or parameterized statement is a feature used to execute the same or similar database statements repeatedly with high efficiency. Typically used with SQL statements such as queries or updates, the prepared statement takes the form of a template into which certain constant values are substituted during each execution.

The typical workflow of using a prepared statement is as follows:
1.Prepare: The statement template is created by the application and sent to the database management system (DBMS). Certain values are left unspecified, called parameters, placeholders or bind variables (labelled "?" below): INSERT INTO PRODUCT (name, price) VALUES (?, ?)

2.The DBMS parses, compiles, and performs query optimization on the statement template, and stores the result without executing it.
3.Execute: At a later time, the application supplies (or binds) values for the parameters, and the DBMS executes the statement (possibly returning a result). The application may execute the statement as many times as it wants with different values. In this example, it might supply 'Bread' for the first parameter and '1.00' for the second parameter.


可以看出,使用preparedStatement的典型工作流程有三步:1)準備:應用將有佔位符或繫結變數的sql statement傳送給資料庫管理系統。2)資料庫管理系統解析、編譯、和優化sql statement並將結果(執行計劃)快取。3)執行:應用提供繫結引數的值,由DBMS執行。

以上三步中的1)和3)的提供繫結引數的值都是由JDBC做的,程式碼操作就是如下

PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM product p WHERE p.productName=?");   //prepare
			preparedStatement.setString(1,"Mango");
ResultSet resultSet = preparedStatement.executeQuery();

而JDBC只是提供介面,實現是在資料庫驅動中做的,而資料庫驅動必須支援預編譯才行(MySql需要開啟)。不支援預編譯SQL查詢的JDBC驅動,在呼叫connection.prepareStatement(sql)的時候,它不會把SQL查詢語句傳送給資料庫做預處理,而是等到執行查詢動作的時候(呼叫executeQuery()方法時)才把查詢語句傳送個數據庫,這種情況和使用Statement是一樣的。

4、MySQL驅動開啟預編譯

就以mysql driver分析原始碼:

com.mysql.jdbc.ConnectionImpl.java

/**
     * A SQL statement with or without IN parameters can be pre-compiled and
     * stored in a PreparedStatement object. This object can then be used to
     * efficiently execute this statement multiple times.
     * <p>
     * <B>Note:</B> This method is optimized for handling parametric SQL statements that benefit from precompilation if the driver supports precompilation. In
     * this case, the statement is not sent to the database until the PreparedStatement is executed. This has no direct effect on users; however it does affect
     * which method throws certain java.sql.SQLExceptions
     * </p>
     * <p>
     * MySQL does not support precompilation of statements, so they are handled by the driver.
     * </p>
     * 
     * @param sql
     *            a SQL statement that may contain one or more '?' IN parameter
     *            placeholders
     * @return a new PreparedStatement object containing the pre-compiled
     *         statement.
     * @exception SQLException
     *                if a database access error occurs.
     */
    public java.sql.PreparedStatement prepareStatement(String sql) throws SQLException {
        return prepareStatement(sql, DEFAULT_RESULT_SET_TYPE, DEFAULT_RESULT_SET_CONCURRENCY);
    }

從註釋可以看出預編譯的意義,wiki給出預編譯的優點

As compared to executing SQL statements directly, prepared statements offer two main advantages:[1]
1)The overhead of compiling and optimizing the statement is incurred only once, although the statement is executed multiple times. Not all optimization can be performed at the time the prepared statement is compiled, for two reasons: the best plan may depend on the specific values of the parameters, and the best plan may change as tables and indexes change over time.[2]
2)Prepared statements are resilient against SQL injection, because parameter values, which are transmitted later using a different protocol, need not be correctly escaped. If the original statement template is not derived from external input, SQL injection cannot occur.

可以看出最主要的有兩點1)一次預編譯,多次執行。2)防止sql注入。

繼續看prepareStatement

 public java.sql.PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
        synchronized (getConnectionMutex()) {
            checkClosed();

            //
            // FIXME: Create warnings if can't create results of the given type or concurrency
            //
            PreparedStatement pStmt = null;

            boolean canServerPrepare = true;

            String nativeSql = getProcessEscapeCodesForPrepStmts() ? nativeSQL(sql) : sql;

            if (this.useServerPreparedStmts && getEmulateUnsupportedPstmts()) {
                canServerPrepare = canHandleAsServerPreparedStatement(nativeSql);
            }

            if (this.useServerPreparedStmts && canServerPrepare) {
                if (this.getCachePreparedStatements()) {
                    synchronized (this.serverSideStatementCache) {
                        pStmt = (com.mysql.jdbc.ServerPreparedStatement) this.serverSideStatementCache.remove(sql);

                        if (pStmt != null) {
                            ((com.mysql.jdbc.ServerPreparedStatement) pStmt).setClosed(false);
                            pStmt.clearParameters();
                        }

                        if (pStmt == null) {
                            try {
                                pStmt = ServerPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database, resultSetType,
                                        resultSetConcurrency);
                                if (sql.length() < getPreparedStatementCacheSqlLimit()) {
                                    ((com.mysql.jdbc.ServerPreparedStatement) pStmt).isCached = true;
                                }

                                pStmt.setResultSetType(resultSetType);
                                pStmt.setResultSetConcurrency(resultSetConcurrency);
                            } catch (SQLException sqlEx) {
                                // Punt, if necessary
                                if (getEmulateUnsupportedPstmts()) {
                                    pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);

                                    if (sql.length() < getPreparedStatementCacheSqlLimit()) {
                                        this.serverSideStatementCheckCache.put(sql, Boolean.FALSE);
                                    }
                                } else {
                                    throw sqlEx;
                                }
                            }
                        }
                    }
                } else {
                    try {
                        pStmt = ServerPreparedStatement.getInstance(getMultiHostSafeProxy(), nativeSql, this.database, resultSetType, resultSetConcurrency);

                        pStmt.setResultSetType(resultSetType);
                        pStmt.setResultSetConcurrency(resultSetConcurrency);
                    } catch (SQLException sqlEx) {
                        // Punt, if necessary
                        if (getEmulateUnsupportedPstmts()) {
                            pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);
                        } else {
                            throw sqlEx;
                        }
                    }
                }
            } else {
                pStmt = (PreparedStatement) clientPrepareStatement(nativeSql, resultSetType, resultSetConcurrency, false);
            }

            return pStmt;
        }
    }

首先根據是否開啟預編譯建立ServerPreparedStatement還是clientPrepareStatement判定邏輯是基於“useServerPreparedStmts”、“canServerPrepare”這兩個引數決定的,而“useServerPreparedStmts”我們可以將對應的引數設定為true即可。但是還需要取決於canHandleAsServerPreparedStatement(String sql),可以看出並不是所有的sql都會預編譯,首先只考慮“SELECT、UPDATE、DELETE、INSERT、REPLACE”幾種語法規則,也就是如果不是這幾種就直接返回false了。另外會對引數Limit後面7位做一個判定是否有逗號、?這些符號,如果有這些就返回false了。

對於本例中的sql在url中加入

jdbc.url=jdbc:mysql://${host}:3306/hhl?useServerPrepStmts=true&cachePrepStmts=true&prepStmtCacheSize=25&prepStmtCacheSqlLimit=2048&characterEncoding=utf8&useSSL=false

實際上開啟預編譯返回的PreparedStatement是com.mysql.jdbc.JDBC42ServerPreparedStatement,而未開啟預編譯返回的是com.mysql.jdbc.JDBC42PreparedStatement。


其中還有快取個數及快取sql的長度,預設25個及256(本例設定2048)長,超過這些個數或長度就不會快取預編譯statement,還需要再預編譯一次。對比如下:

不開啟預編譯的mysql中的操作日誌:

2017-05-09T01:03:44.863573Z	    2 Query	SELECT * FROM product p WHERE p.productName='Mango'


開啟預編譯的mysql中的操作日誌

2017-05-09T08:32:27.518405Z	   17 Prepare	SELECT * FROM product p WHERE p.productName=?
2017-05-09T08:32:27.538406Z	   17 Execute	SELECT * FROM product p WHERE p.productName='Mango'
2017-05-09T08:33:22.339540Z	   17 Execute	SELECT * FROM product p WHERE p.productName='Mango'
private void serverPrepare(String sql) throws SQLException {
        synchronized (checkClosed().getConnectionMutex()) {
            MysqlIO mysql = this.connection.getIO();

            if (this.connection.getAutoGenerateTestcaseScript()) {
                dumpPrepareForTestcase();
            }

            try {
                long begin = 0;

                if (StringUtils.startsWithIgnoreCaseAndWs(sql, "LOAD DATA")) {
                    this.isLoadDataQuery = true;
                } else {
                    this.isLoadDataQuery = false;
                }

                if (this.connection.getProfileSql()) {
                    begin = System.currentTimeMillis();
                }

                String characterEncoding = null;
                String connectionEncoding = this.connection.getEncoding();

                if (!this.isLoadDataQuery && this.connection.getUseUnicode() && (connectionEncoding != null)) {
                    characterEncoding = connectionEncoding;
                }

                Buffer prepareResultPacket = mysql.sendCommand(MysqlDefs.COM_PREPARE, sql, null, false, characterEncoding, 0);

                if (this.connection.versionMeetsMinimum(4, 1, 1)) {
                    // 4.1.1 and newer use the first byte as an 'ok' or 'error' flag, so move the buffer pointer past it to start reading the statement id.
                    prepareResultPacket.setPosition(1);
                } else {
                    // 4.1.0 doesn't use the first byte as an 'ok' or 'error' flag
                    prepareResultPacket.setPosition(0);
                }

                this.serverStatementId = prepareResultPacket.readLong();
                this.fieldCount = prepareResultPacket.readInt();
                this.parameterCount = prepareResultPacket.readInt();
                this.parameterBindings = new BindValue[this.parameterCount];

                for (int i = 0; i < this.parameterCount; i++) {
                    this.parameterBindings[i] = new BindValue();
                }

                this.connection.incrementNumberOfPrepares();

                if (this.profileSQL) {
                    this.eventSink.consumeEvent(new ProfilerEvent(ProfilerEvent.TYPE_PREPARE, "", this.currentCatalog, this.connectionId, this.statementId, -1,
                            System.currentTimeMillis(), mysql.getCurrentTimeNanosOrMillis() - begin, mysql.getQueryTimingUnits(), null, LogUtils
                                    .findCallingClassAndMethod(new Throwable()), truncateQueryToLog(sql)));
                }

                if (this.parameterCount > 0) {
                    if (this.connection.versionMeetsMinimum(4, 1, 2) && !mysql.isVersion(5, 0, 0)) {
                        this.parameterFields = new Field[this.parameterCount];

                        Buffer metaDataPacket = mysql.readPacket();

                        int i = 0;

                        while (!metaDataPacket.isLastDataPacket() && (i < this.parameterCount)) {
                            this.parameterFields[i++] = mysql.unpackField(metaDataPacket, false);
                            metaDataPacket = mysql.readPacket();
                        }
                    }
                }

                if (this.fieldCount > 0) {
                    this.resultFields = new Field[this.fieldCount];

                    Buffer fieldPacket = mysql.readPacket();

                    int i = 0;

                    // Read in the result set column information
                    while (!fieldPacket.isLastDataPacket() && (i < this.fieldCount)) {
                        this.resultFields[i++] = mysql.unpackField(fieldPacket, false);
                        fieldPacket = mysql.readPacket();
                    }
                }
            } catch (SQLException sqlEx) {
                if (this.connection.getDumpQueriesOnException()) {
                    StringBuilder messageBuf = new StringBuilder(this.originalSql.length() + 32);
                    messageBuf.append("\n\nQuery being prepared when exception was thrown:\n\n");
                    messageBuf.append(this.originalSql);

                    sqlEx = ConnectionImpl.appendMessageToException(sqlEx, messageBuf.toString(), getExceptionInterceptor());
                }

                throw sqlEx;
            } finally {
                // Leave the I/O channel in a known state...there might be packets out there that we're not interested in
                this.connection.getIO().clearInputStream();
            }
        }
    }


最終將sql傳送給資料庫管理系統進行prepare操作。

小結:1)根據是否開啟預編譯和對該sql是否預編譯決定不同的preparedstatement操作,預編譯並儲存到快取中(沒有超過快取個數和長度) 2)後面操作類似sql時會從快取中取出preparedstatement,如果快取中沒有則再發送給資料庫管理系統預編譯,也就是會執行多個prepare(資料庫日誌中可以看出)

2017-05-10T06:22:21.552185Z	   28 Prepare	SELECT * FROM product p WHERE p.productName=?
2017-05-10T06:25:18.132574Z	   28 Execute	SELECT * FROM product p WHERE p.productName='Mango'
2017-05-10T06:25:19.222636Z	   26 Prepare	SELECT * FROM product p WHERE p.productName=?
2017-05-10T06:25:21.599731Z	   26 Execute	SELECT * FROM product p WHERE p.productName='Mango'

什麼時候快取的,就是在呼叫preparedstatement.close()的時候com.mysql.jdbc.ServerPreparedStatement.java

/**
     * @see java.sql.Statement#close()
     */
    @Override
    public void close() throws SQLException {
        MySQLConnection locallyScopedConn = this.connection;

        if (locallyScopedConn == null) {
            return; // already closed
        }

        synchronized (locallyScopedConn.getConnectionMutex()) {

            if (this.isCached && !this.isClosed) {
                clearParameters();

                this.isClosed = true;

                this.connection.recachePreparedStatement(this);    //快取
                return;
            }

            realClose(true, true);
        }
    }

可見快取是快取到連線快取中的。如果連線關閉或者重新建立資料庫連線,那麼快取失效,比如下面操作:

try {
			Class.forName("com.mysql.jdbc.Driver");
			Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/hhl?useServerPrepStmts=true&cachePrepStmts=true&prepStmtCacheSize=25&prepStmtCacheSqlLimit=2048&characterEncoding=utf8&useSSL=false",
					"root", "123456");
			PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM product p WHERE p.productName=?");
			preparedStatement.setString(1,"'Mango'");
			ResultSet resultSet = preparedStatement.executeQuery();
			String productId;
			while (resultSet.next()){
				productId = (String) resultSet.getObject(1);
				System.out.println(productId);
			}
			resultSet.close();
			preparedStatement.close();
			connection.close();

		} catch (Exception e) {
			e.printStackTrace();
		}

每次都會重新建立資料庫連線,那麼每次都會預編譯,日誌如下:

2017-05-11T02:03:11.344672Z	    8 Connect	[email protected] on hhl using TCP/IP
2017-05-11T02:03:11.344672Z	    8 Query	/* mysql-connector-java-5.1.38 ( Revision: fe541c166cec739c74cc727c5da96c1028b4834a ) */SELECT  @@session.auto_increment_increment AS auto_increment_increment, @@character_set_client AS character_set_client, @@character_set_connection AS character_set_connection, @@character_set_results AS character_set_results, @@character_set_server AS character_set_server, @@init_connect AS init_connect, @@interactive_timeout AS interactive_timeout, @@license AS license, @@lower_case_table_names AS lower_case_table_names, @@max_allowed_packet AS max_allowed_packet, @@net_buffer_length AS net_buffer_length, @@net_write_timeout AS net_write_timeout, @@query_cache_size AS query_cache_size, @@query_cache_type AS query_cache_type, @@sql_mode AS sql_mode, @@system_time_zone AS system_time_zone, @@time_zone AS time_zone, @@tx_isolation AS tx_isolation, @@wait_timeout AS wait_timeout
2017-05-11T02:03:11.344672Z	    8 Query	SET NAMES utf8
2017-05-11T02:03:11.344672Z	    8 Query	SET character_set_results = NULL
2017-05-11T02:03:11.344672Z	    8 Query	SET autocommit=1
2017-05-11T02:03:14.495877Z	    8 Prepare	SELECT * FROM product p WHERE p.productName=?
2017-05-11T02:03:17.116682Z	    8 Execute	SELECT * FROM product p WHERE p.productName='\'Mango\''
2017-05-11T02:03:18.349084Z	    8 Quit	
2017-05-11T02:03:25.841572Z	    9 Connect	[email protected] on hhl using TCP/IP
2017-05-11T02:03:25.842572Z	    9 Query	/* mysql-connector-java-5.1.38 ( Revision: fe541c166cec739c74cc727c5da96c1028b4834a ) */SELECT  @@session.auto_increment_increment AS auto_increment_increment, @@character_set_client AS character_set_client, @@character_set_connection AS character_set_connection, @@character_set_results AS character_set_results, @@character_set_server AS character_set_server, @@init_connect AS init_connect, @@interactive_timeout AS interactive_timeout, @@license AS license, @@lower_case_table_names AS lower_case_table_names, @@max_allowed_packet AS max_allowed_packet, @@net_buffer_length AS net_buffer_length, @@net_write_timeout AS net_write_timeout, @@query_cache_size AS query_cache_size, @@query_cache_type AS query_cache_type, @@sql_mode AS sql_mode, @@system_time_zone AS system_time_zone, @@time_zone AS time_zone, @@tx_isolation AS tx_isolation, @@wait_timeout AS wait_timeout
2017-05-11T02:03:25.843572Z	    9 Query	SET NAMES utf8
2017-05-11T02:03:25.844572Z	    9 Query	SET character_set_results = NULL
2017-05-11T02:03:25.845572Z	    9 Query	SET autocommit=1
2017-05-11T02:03:32.180935Z	    9 Prepare	SELECT * FROM product p WHERE p.productName=?
2017-05-11T02:03:36.316171Z	    9 Execute	SELECT * FROM product p WHERE p.productName='\'Mango\''
2017-05-11T02:03:37.224223Z	    9 Quit	

而採用連線池可以一定程度上避免出現這種情況。
2)可見如果不採用預編譯則驅動會將sql拼裝起來一次(preparedStatement.executeQuery()時)傳送給資料庫(由對應的mysql驅動com.mysql.jdbc.JDBC42PreparedStatement實現,傳送的是SELECT * FROM product p WHERE p.productName='\'Mango\''包),而採用預編譯的會和資料庫互動兩次,第一次是(connection.prepareStatement("SELECT * FROM product p WHERE p.productName=?")時)傳送sql進行預編譯(傳送的是SELECT * FROM product p WHERE p.productName=?包),第二次執行的時候會(preparedStatement.executeQuery())傳送繫結引數(由對應的mysql驅動com.mysql.jdbc.JDBC42ServerPreparedStatement實現,傳送的是'Mango'包,而沒有進行轉義的,這個轉義是在資料庫管理系統中去做的。),如果單純的一次查詢則不預編譯的效率要高,而且如果預編譯不快取的話下次還需要預編譯。因此對於一次查詢的建議不開啟預編譯,對已多次查詢開啟預編譯的同時也要開啟快取。

預編譯時那麼資料庫是怎麼知道要執行哪條sql語句的,就在於傳送包中的

/** The ID that the server uses to identify this PreparedStatement */
    private long serverStatementId;

該值是傳送預編譯語句時資料庫返回的

Buffer prepareResultPacket = mysql.sendCommand(MysqlDefs.COM_PREPARE, sql, null, false, characterEncoding, 0);

             
                this.serverStatementId = prepareResultPacket.readLong();


看完預編譯,看下不採用預編譯的prepared.setString(),

**
     * Set a parameter to a Java String value. The driver converts this to a SQL
     * VARCHAR or LONGVARCHAR value (depending on the arguments size relative to
     * the driver's limits on VARCHARs) when it sends it to the database.
     * 
     * @param parameterIndex
     *            the first parameter is 1...
     * @param x
     *            the parameter value
     * 
     * @exception SQLException
     *                if a database access error occurs
     */
    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('\'');

                        if (!this.isLoadDataQuery) {
                            parameterAsBytes = StringUtils.getBytes(quotedString.toString(), this.charConverter, this.charEncoding,
                                    this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), getExceptionInterceptor());
                        } else {
                            // Send with platform character encoding
                            parameterAsBytes = StringUtils.getBytes(quotedString.toString());
                        }

                        setInternal(parameterIndex, parameterAsBytes);
                    } else {
                        byte[] parameterAsBytes = null;

                        if (!this.isLoadDataQuery) {
                            parameterAsBytes = StringUtils.getBytes(x, this.charConverter, this.charEncoding, this.connection.getServerCharset(),
                                    this.connection.parserKnowsUnicode(), getExceptionInterceptor());
                        } else {
                            // Send with platform character encoding
                            parameterAsBytes = StringUtils.getBytes(x);
                        }

                        setBytes(parameterIndex, parameterAsBytes);
                    }

                    return;
                }

                String parameterAsString = x;
                boolean needsQuoted = true;

                if (this.isLoadDataQuery || isEscapeNeededForString(x, stringLength)) {
                    needsQuoted = false; // saves an allocation later

                    StringBuilder buf = new StringBuilder((int) (x.length() * 1.1));

                    buf.append('\'');

                    //
                    // Note: buf.append(char) is _faster_ than appending in blocks, because the block append requires a System.arraycopy().... go figure...
                    //

                    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 '\n': /* Must be escaped for logs */
                                buf.append('\\');
                                buf.append('n');

                                break;

                            case '\r':
                                buf.append('\\');
                                buf.append('r');

                                break;

                            case '\\':
                                buf.append('\\');
                                buf.append('\\');

                                break;

                            case '\'':
                                buf.append('\\');
                                buf.append('\'');

                                break;

                            case '"': /* Better safe than sorry */
                                if (this.usingAnsiMode) {
                                    buf.append('\\');
                                }

                                buf.append('"');

                                break;

                            case '\032': /* This gives problems on Win32 */
                                buf.append('\\');
                                buf.append('Z');

                                break;

                            case '\u00a5':
                            case '\u20a9':
                                // escape characters interpreted as backslash by mysql
                                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) == '\\') {
                                        buf.append('\\');
                                    }
                                }
                                // fall through

                            default:
                                buf.append(c);
                        }
                    }

                    buf.append('\'');

                    parameterAsString = buf.toString();
                }

                byte[] parameterAsBytes = null;

                if (!this.isLoadDataQuery) {
                    if (needsQuoted) {
                        parameterAsBytes = StringUtils.getBytesWrapped(parameterAsString, '\'', '\'', this.charConverter, this.charEncoding,
                                this.connection.getServerCharset(), this.connection.parserKnowsUnicode(), getExceptionInterceptor());
                    } else {
                        parameterAsBytes = StringUtils.getBytes(parameterAsString, this.charConverter, this.charEncoding, this.connection.getServerCharset(),
                                this.connection.parserKnowsUnicode(), getExceptionInterceptor());
                    }
                } else {
                    // Send with platform character encoding
                    parameterAsBytes = StringUtils.getBytes(parameterAsString);
                }

                setInternal(parameterIndex, parameterAsBytes);

                this.parameterTypes[parameterIndex - 1 + getParameterIndexOffset()] = Types.VARCHAR;
            }
        }
    }

會對特殊字元進行轉義,從而防止sql注入,如preparedStatement.setString(1,"'Mango'");經轉義後變為\'Mango\'。轉碼工作是有JDBC驅動或者資料庫進行的,而且無論是否開啟預編譯,只要使用PreparedStatement設定引數的形式,都會對sql進行轉義,都會防止sql注入。

5、SQL注入

假如不用PreparedStatement進行引數設定,採用拼接的形式,如下

String sql = "SELECT * FROM product p WHERE p.productName='" + productName + "'";
如果使用者沒有輸入一個名字而是輸入
Mango' or 'Y' = 'Y

產生的語句就變成了

SELECT * FROM product p WHERE p.productName='Mango' or 'Y' = 'Y'

這樣就改變了查詢的本意,會返回整個product關係。更甚者可以編寫輸入值以輸出更多的資料,使用preparedStatement可以防止這類問題,被轉義後的語句變為如下:

SELECT * FROM product p WHERE p.productName='Mango\' or \'Y\' = \'Y'

這樣就變成一個無害的語句

6、Hibernate引數繫結

Hibernate的實現也是基於JDBC的,而且採用的是PreparedStatement的方式,因此Hibernate的引數繫結具有PreparedStatement的所有優點。Hibernate引數繫結的方式有兩種:利用具名或者是利用定位

1)具名方式

/* (non-Javadoc)
	 * @see com.mango.jtt.dao.MangoDao#list(java.lang.String)
	 */
	@Override
	public List list(String querySql, Map<String, Object> map) {
		Query<?> query = currentSession().createQuery(querySql);
		if (map != null) {
			for (String key : map.keySet()) {
				if (querySql.indexOf(":" + key) != -1) {
					query.setParameter(key, map.get(key));       	
				}
			}
		}
		return query.getResultList();
	}

使用

/* (non-Javadoc)
	 * @see com.mango.jtt.service.ProductService#getProductList()
	 */
	@Override
	public List<Product> getProductList() {
		String sql = "from Product p where p.productName=:productName ";
		Map<String, Object> map = new HashMap<String, Object>();
		map.put("productName", "Mango");
		return dao.list(sql, map);
	}



2)定位引數方式

String sql = "from product p where p.productName=?";
Query query = session.creatQuery(sql).setparameter(0, "Mango");

同樣引數繫結具有防止SQL注入的作用。

總結:

1)一般關係資料庫(Oracle、MySQL)是支援預編譯的,預編譯sql操作意味著資料庫系統不用再進行分析,直接從快取中取出執行計劃執行即可,提高效率

2)本文中的MySql是需要驅動端開啟預編譯功能的,否則預設不進行預編譯,是否預編譯由驅動控制,語句的預編譯時在資料庫系統中做的,並將預編譯語句快取在資料庫快取中;而應用中快取的是對應的PrepareStatement,是快取在資料庫連線中的。

3)是否採用預編譯對SQL注入沒有影響,只要使用PreparedStatement設定引數方式操作資料庫,則均能防止SQL注入



相關推薦

Hibernate實戰原始碼解析Hibernate引數PreparedStatementSQL注入原理

    本文采用mysql驅動是5.1.38版本。    本篇文章涉及內容比較多,單就Hibernate來講就很大,再加上資料庫驅動和資料庫相關,非一篇文章或一篇專題就能說得完。本文從使用入手在【Spring實戰】----Spring4.3.2整合Hibernate5.2

Java實戰原始碼解析為什麼覆蓋equals方法時總要覆蓋hashCode方法

1、背景知識本文程式碼基於jdk1.8分析,《Java程式設計思想》中有如下描述:另外再看下Object.java對hashCode()方法的說明:/** * Returns a hash code value for the object. This method

Spring實戰----原始碼解析SessionFactorySession的管理getCurrentSession的使用

在上一篇Hibernate5整合中當使用sessionFactory.getCurrentSession()時會報錯Could not obtain transaction-synchronized Session for current thread,本篇就從原始碼角度

Vue原始碼分析--雙向資料的實現

總結 Vue的雙向資料繫結主要通過Object.defineProperty來實現,先為所有的屬性加上get/set的監控,這樣當屬性值改變時就會觸發對應的set方法,然後再在set方法中通過觀

underscore.js原始碼解析之函式

1. 引言   underscore.js是一個1500行左右的Javascript函式式工具庫,裡面提供了很多實用的、耦合度極低的函式,用來方便的操作Javascript中的陣列、物件和函式,它支援函式式和麵向物件鏈式的程式設計風格,還提供了一個精巧的模板引

Android7.1.2原始碼解析系列實戰分析init.rc檔案

實戰分析init.rc檔案 前言:經過上一篇的/system/core/init/readme.txt檔案的翻譯,對於init.rc的語法也有了一定的瞭解,這一篇就對/system/core/rootdir/init.rc檔案進行一個分析,希望能夠藉此對android的開

Spring實戰Spring註解配置工作原理原始碼解析

一、背景知識在【Spring實戰】Spring容器初始化完成後執行初始化資料方法一文中說要分析其實現原理,於是就從原始碼中尋找答案,看原始碼容易跑偏,因此應當有個主線,或者帶著問題、目標去看,這樣才能最大限度的提升自身程式碼水平。由於上文中大部分都基於註解進行設定的(Spri

SSH 基礎淺談Hibernate關系映射(3)

區別 ack 增加 ans 存儲結構 mil pro 映射 方向 繼上篇博客 一對多關聯映射(單向) 上面我們介紹了多對一,我們反過來看一對多不就是多對一嗎?那還用再進行不同的映射嗎?有什麽區別嗎?一對多和多對一映射原理是一致的,存儲是同樣的。也就是生成的數據庫

持久化框架Mybatis與Hibernate的詳細對比

很大的 效率 myba 今天 http 目的 ping pin 增刪 作為一位優秀的程序員,只知道一種ORM框架是遠遠不夠的。在開發項目之前,架構的技術選型對於項目是否成功起到至關重要的作用。我們不僅要了解同類型框架的原理以及技術實現,還要深入的理解各自的優缺點,以便我們能

基礎+實戰JVM原理優化系列之八:如何檢視JVM引數配置?

1. 檢視JAVA版本資訊 2. 檢視JVM執行模式  在$JAVA_HOME/jre/bin下有client和server兩個目錄,分別代表JVM的兩種執行模式。   client執行模式,針對桌面應用,載入速度比server模式快10%,而執行速度為server模

Gson原始碼解析

private FieldNamingStrategy fieldNamingPolicy = FieldNamingPolicy.IDENTITY; public Gson create() { List<TypeAdapterFactory> facto

MyBatis原始碼分析TypeHandler解析屬性配置元素詳述相關列舉使用高階進階

TypeHandler解析接著看一下typeHandlerElement(root.evalNode("typeHandlers"));方法,這句讀取的是<configuration>下的<typeHandlers>節點,程式碼實現為:private

逆向實戰解析與利用安卓載荷_metasploit

/轉載請註明原作者:Kali_MG1937 QQ3496925334/ metasploit大家都知道, 其中的一個payload: android/meterpreter/reverse_tcp 就是安卓載荷了,msf可通過傳送tcp包來控制安裝了此病毒的手機 ●上電腦課的時候閒著無聊,就

C語言main函式的引數解析

main函式 每個C程式都必須有一個main函式,main函式又稱為主函式,是執行程式的起點,它被稱之為函式,是否會像平時使用函式時需要自己的引數呢? 答案是肯定的,那麼他都有那些引數呢? main函式的在vs2017環境下除錯,可以看到main函式裡的三個引數

Spring實戰----Spring事務管理配置解析

上篇說了aop的配置,並且說了Spring事務管理是基於aop的,那麼Spring宣告式事務的配置就有兩種方式:XML配置及註解配置不多說,直接看配置檔案一、配置檔案applicationContext-transaction.xml<?xml version="1.0

Android7.1.2原始碼解析系列android init目錄下的Android.mk編譯檔案分析

上一篇文章對於原始碼中的安卓編譯系統文件進行了翻譯,本文就以android當中的init模組作為例子,對其中的Android.mk檔案進行分析,讀者可以在閱讀本文的同時檢視我的譯文:http://blog.csdn.net/class_brick/article/detai

Android原始碼解析View.post()

emmm,大夥都知道,子執行緒是不能進行 UI 操作的,或者很多場景下,一些操作需要延遲執行,這些都可以通過 Handler 來解決。但說實話,實在是太懶了,總感覺寫 Handler 太麻煩了,一不小心又很容易寫出記憶體洩漏的程式碼來,所以為了偷懶,我就經常用 View.

Android實戰----從Retrofit原始碼分析到Java網路程式設計以及HTTP權威指南想到的

一、簡介        接上一篇【Android實戰】----基於Retrofit實現多圖片/檔案、圖文上傳 中曾說非常想搞明白為什麼Retrofit那麼屌。最近也看了一些其原始碼分析的文章以及親自查看了原始碼,發現其對Java網路程式設計及HTTP權威指南有了一個很好的詮釋

Spring實戰----Spring配置檔案的解析

一、背景知識Spring的核心的核心就是bean的配置及管理,至Spring最新發布的版本4.3.2已經有三種方式可以配置bean:1)在XML中進行顯示配置2)在Java中進行顯示配置3)隱式的bean發現機制和自動裝配上述三種配置不展開說明,而且目前用的較多的是第3種(當

Hibernate原始碼解析 Hibernate中的動態代理Javassist

今天,我們來看看Hibernate中的動態代理,對動態代理有興趣的朋友可以讀一讀,看看Hibernate的作者們是怎麼實現的,為我們之後用的時候提供一點思路。正如你所知Hibernate動態代理並不是用Java原生的方式,而是用了Javassist。Javassist可以