MyBatis中#{}
和${}
的深度解析:SQL注入與動態拼接的終極抉擇
摘要:在MyBatis的Mapper.xml文件中,
#{}
和${}
這兩個看似簡單的符號,卻隱藏著SQL安全與性能的核心秘密。本文將深入剖析它們的底層差異,并通過真實場景演示如何正確選擇,避免致命的安全漏洞!
一、符號初探:表面相似,本質不同
在MyBatis的SQL編寫中,我們經常看到這樣的寫法:
<!-- 使用# -->
<select id="getUser" resultType="User">SELECT * FROM user WHERE id = #{id}
</select><!-- 使用$ -->
<select id="getUser" resultType="User">SELECT * FROM user WHERE name = ${name}
</select>
表面看:兩者都用于參數替換
本質區別:它們的處理機制天差地別!
特性 | #{} | ${} |
---|---|---|
處理方式 | 預編譯參數(PreparedStatement) | 字符串直接替換 |
防SQL注入 | ? 安全 | ? 高風險 |
數據類型轉換 | 自動類型轉換 | 需手動加引號 |
適用場景 | 值傳遞(WHERE條件等) | 動態SQL片段(表名等) |
二、底層原理:安全與危險的根源
1. #{}
的預編譯機制(安全衛士)
// MyBatis實際執行代碼(簡化版)
PreparedStatement ps = conn.prepareStatement("SELECT * FROM user WHERE id=?");
ps.setInt(1, 5); // 安全!參數被嚴格處理
執行流程:
- 將SQL語句編譯為模板
- 參數作為獨立數據傳入
- 數據庫引擎嚴格區分指令和數據
2. ${}
的字符串替換(危險陷阱)
// 假設傳入 name = "admin' OR '1'='1"
String sql = "SELECT * FROM user WHERE name=" + name;
// 最終SQL:SELECT * FROM user WHERE name='admin' OR '1'='1'
注入風險:攻擊者可通過精心構造參數執行任意SQL!
三、實戰對比:當$
遭遇SQL注入攻擊
場景:用戶登錄驗證
<!-- 危險寫法 -->
<select id="login" resultType="User">SELECT * FROM users WHERE username = ${username} AND password = ${password}
</select>
攻擊者輸入:
username = "admin' -- "
password = "anything"
生成的致命SQL:
SELECT * FROM users
WHERE username = 'admin' -- ' AND password = 'anything'
結果:攻擊者無需密碼直接登錄管理員賬戶!
修復方案(改用#
):
<select id="login" resultType="User">SELECT * FROM users WHERE username = #{username} AND password = #{password}
</select>
此時攻擊輸入將被轉義為:
WHERE username = 'admin'' -- ' AND ...
數據庫會嚴格查找用戶名為 admin' --
的記錄,攻擊失效!
四、${}
的正確打開方式:動態元數據操作
雖然${}
有風險,但在特定場景下不可替代:
場景1:動態表名
<select id="getLogsByTable" resultType="Log">SELECT * FROM ${tableName} WHERE year = #{year}
</select>
注:表名是SQL指令的一部分,無法使用預編譯占位符
場景2:動態排序
<select id="getUsers" resultType="User">SELECT * FROM usersORDER BY ${sortColumn} ${sortOrder}
</select>
安全規范:
- 白名單校驗:在Java代碼中校驗傳入的元數據
// 表名白名單校驗 Set<String> validTables = Set.of("log_2023","log_2024"); if(!validTables.contains(tableName)) {throw new IllegalArgumentException("Invalid table name"); }
- 避免用戶輸入:動態參數應來自系統內部,而非前端直接傳入
五、性能對比:#
vs $
的隱藏差異
操作 | #{} | ${} |
---|---|---|
SQL編譯 | 首次編譯模板,后續復用 | 每次生成全新SQL |
數據庫緩存 | 相同SQL模板可復用執行計劃 | 每次被視為不同SQL,無法復用 |
執行10萬次 | 編譯1次 + 執行10萬次 | 編譯10萬次 + 執行10萬次 |
典型耗時 | ≈1.5秒 | ≈15秒(10倍差距!) |
實測結論:高并發場景下,
#{}
的性能優勢極為明顯!
六、黃金法則:如何選擇符號
-
優先使用
#{}
- WHERE條件中的值
- INSERT/UPDATE的字段值
- 所有用戶輸入參數
-
謹慎使用
${}
- 動態表名/列名
- ORDER BY排序子句
- SQL關鍵字(如LIMIT)
- 必須確保參數值內部可控!
-
絕對禁止
<!-- 禁止將用戶輸入直接用于$ --> WHERE username = ${userInput} ?
七、擴展技巧:#
的高級用法
1. 類型處理器指定
<!-- 強制使用String類型處理器 -->
#{age, javaType=int, jdbcType=NUMERIC}
2. 日期格式轉換
#{createTime, jdbcType=TIMESTAMP, pattern="yyyy-MM-dd"}
3. 非空校驗
<!-- 當email為空時設置默認值 -->
#{email, jdbcType=VARCHAR, default='no-email@domain.com'}
結語
#{}
和${}
的選擇本質是安全與靈活性的權衡:
#{}
是默認首選,保障安全與性能${}
是特定場景下的"手術刀",需嚴格管控
牢記:一次錯誤的
${}
使用可能導致整個系統淪陷!建議在團隊中制定《SQL編寫規范》,并配合SQL掃描工具(如SQLMap)定期檢測漏洞。
技術討論:你在項目中遇到過哪些${}
引發的安全問題?歡迎評論區分享避坑經驗!