首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

MyBatis #{} 與 ${} 核心差異深度解析:預處理語句防注入 vs 字符串直接替換

     編輯:Java 相關知識

前言:一個符號差異,關係資料庫安全

MyBatis 映射檔中 #{} 與 ${} 看似相似,實則機制截然不同:前者生成預處理語句 (PreparedStatement),參數化查詢,徹底防 SQL 注入;後者為字符串直接替換 (Statement),拼接 SQL 片段,極高風險。本文將從原理、適用場景、安全性、實戰案例四個維度徹底釐清,並給出「能用 # 絕不用 $」的鐵律與例外處理指南。


核心機制對照表

維度#{param} (預處理)${param} (字符串替換)
底層實現JDBC PreparedStatement 參數佔位符 ?MyBatis 解析階段直接字符串替換
SQL 結構編譯期固定,參數傳遞不影響結構替換後重新編譯,結構可被改變
類型處理自動匹配 JDBC 類型,字符串自動加引號原樣輸出,字符串需手動加引號
SQL 注入防護✅ 徹底防禦 (參數與代碼分離)❌ 完全無防護,極易注入
性能可緩存執行計劃,重複執行高效每次重新編譯,無法緩存計劃
適用場景99% 參數傳遞 (WHERE 值、INSERT 值、UPDATE 值)動態 SQL 結構 (表名、欄位名、ORDER BY 欄位、分頁關鍵字)

實戰案例演示

案例 1:查詢用戶 (安全用法)

<!-- 安全:#{username} 生成 PreparedStatement --> <select id="getUser" resultType="User"> SELECT * FROM user WHERE username = #{username} </select> // 傳入 "admin' OR '1'=1" // 實際 SQL: SELECT * FROM user WHERE username = ? // 參數綁定: "admin' OR '1'=1" // 結果: 查詢字面用戶名為 "admin' OR '1'=1" 的記錄,無注入風險

案例 2:動態排序 (必須用 $)

<!-- 必須用 $:ORDER BY 欄位名無法參數化 --> <select id="listUsers" resultType="User"> SELECT * FROM user <if test="sortField != null"> ORDER BY ${sortField} ${sortOrder} </if> </select> // 傳入 sortField="create_time", sortOrder="DESC" // 實際 SQL: SELECT * FROM user ORDER BY create_time DESC // 注意:sortField 必須經過白名單驗證,防止注入!

案例 3:動態表名 (必須用 $)

<select id="queryLog" resultType="Log"> SELECT * FROM ${tablePrefix}_log WHERE create_time > #{startTime} </select> // tablePrefix 經白名單驗證後傳入

案例 4:LIKE 模糊查詢 (兩種寫法)

<!-- 寫法 1:#{} + CONCAT (推薦,安全) --> SELECT * FROM user WHERE name LIKE CONCAT('%', #{keyword}, '%') <!-- 寫法 2:#{} 直接拼接 % (MyBatis 會自動處理) --> SELECT * FROM user WHERE name LIKE #{keyword} // Java 代碼傳入: "%張三%" // 寫法 3:${} (不推薦,需手動加引號且防注入) --> SELECT * FROM user WHERE name LIKE '%${keyword}%'

安全鐵律與最佳實踐

🛡️ 核心原則:能用 #{} 絕不使用 ${}

  1. 所有值參數 (WHERE 條件值、INSERT/UPDATE 值) → 強制 #{}
  2. 動態 SQL 結構 (表名、欄位名、ORDER BY、GROUP BY、LIMIT/OFFSET 關鍵字) → 只能 ${},但必須配合白名單驗證
  3. 白名單驗證範例:
    // Service 層驗證 public List<User> listUsers(String sortField, String sortOrder) { Set<String> allowedFields = Set.of("id", "name", "create_time", "status"); Set<String> allowedOrders = Set.of("ASC", "DESC"); if (!allowedFields.contains(sortField)) throw new IllegalArgumentException("非法排序欄位"); if (!allowedOrders.contains(sortOrder.toUpperCase())) throw new IllegalArgumentException("非法排序方向"); return mapper.listUsers(sortField, sortOrder); }
  4. 日誌輸出:開啟 MyBatis 日誌 (log4j.logger.org.apache.ibatis=DEBUG) 觀察實際 SQL,確認 #{} 生成 ? 佔位符。

常見誤區澄清

Q: #{} 會自動給字符串加單引號嗎?
A: 不會「加引號」,而是通過 PreparedStatement.setString() 綁定參數,JDBC 驅動負責正確處理類型與轉義,從根本上分離代碼與數據。
Q: ${} 只能用在 SELECT 嗎?
A: 可用於任何 SQL 位置 (INSERT 表名、UPDATE 欄位名、DDL 等),但風險極高,嚴禁用戶輸入直接傳入。
Q: 模糊查詢 LIKE '%#{keyword}%' 為什麼不行?
A: #{} 會被解析為 ?,導致 SQL 變為 LIKE '?%' (引號包住佔位符),語法錯誤。正確寫法見案例 4。
Q: MyBatis Plus / Lambda Query 怎麼處理?
A: 內部同樣基於 #{} 參數化,動態排序欄位同樣需白名單驗證。

總結

#{} = PreparedStatement 參數化 = 安全、高效、標準 (99% 場景)
${} = 字符串模板替換 = 危險、僅限結構動態 (1% 場景 + 白名單)
牢記此鐵律,將 SQL 注入風險拒之門外。代碼審查時,看到 ${} 即為高危點,必須驗證白名單邏輯。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved