MyBatis中#{}和${}的區(qū)別及底層原理、適用場景和安全風(fēng)險(xiǎn)
在 MyBatis 的面試和日常開發(fā)中,參數(shù)占位符 #{} 和 ${} 的區(qū)別是繞不開的核心考點(diǎn)。很多同學(xué)只知道“#{} 安全,${} 不安全”,但其背后的底層原理和適用場景卻一知半解。本文將通過底層分析、日志演示以及 SQL 注入實(shí)驗(yàn),帶你徹底搞定這個知識點(diǎn)。
一、 本質(zhì)區(qū)別速覽
| 特性 | #{}(井號) | ${}(刀樂/美元符號) |
|---|---|---|
| 底層原理 | JDBC 預(yù)編譯占位符 ? | 純字符串拼接,直接替換 |
| SQL 注入 | 安全,自動轉(zhuǎn)義特殊字符 | 危險(xiǎn),容易被惡意篡改 |
| 單引號處理 | 自動添加,無需手動處理 | 不自動加,字符串必須手動加 '' |
| 性能 | 高(預(yù)編譯 SQL 可重復(fù)利用) | 低(每次都需要重新解析) |
| 使用場景 | 99% 的業(yè)務(wù)參數(shù)(Where/Set 值) | SQL 結(jié)構(gòu)關(guān)鍵字(表名、排序字段) |
二、 核心原理詳解
1. #{}:預(yù)編譯模式(推薦)
當(dāng) MyBatis 遇到 #{xxx} 時,它會將 SQL 發(fā)送到數(shù)據(jù)庫進(jìn)行預(yù)編譯。在執(zhí)行階段,再通過 PreparedStatement 設(shè)置參數(shù)。
- 示例代碼:
@Select("select * from user_info where username = #{name}")
UserInfo queryByName(String name);
- 打印日志(重點(diǎn)):
通過日志可以觀察到,SQL 中參數(shù)部分是?占位符:PREPARE: select * from user_info where username = ? - 優(yōu)勢:由于 SQL 結(jié)構(gòu)已固定,傳入的參數(shù)只會被當(dāng)作“值”處理,不會破壞 SQL 語義,從而徹底杜絕 SQL 注入。
2. ${}:字符串拼接模式(慎用)
${} 會在 SQL 執(zhí)行前,直接把參數(shù)原封不動地替換進(jìn) SQL 語句中。
- 示例代碼(報(bào)錯預(yù)警):
@Select("select * from user_info where username = ${name}")
UserInfo queryByName(String name);
- 運(yùn)行結(jié)果:如果你傳入
admin,生成的 SQL 是where username = admin。因?yàn)槿鄙賳我?,?shù)據(jù)庫會報(bào)錯。 - 正確寫法:必須手動加引號:
'${name}'。
三、 SQL 注入(必考面試點(diǎn))
SQL 注入是指攻擊者通過在輸入框中填入 SQL 片段,篡改原有邏輯的行為。
注入場景:免密登錄
假設(shè)我們有一段使用 ${} 的危險(xiǎn)代碼:
SELECT * FROM user WHERE username = '${name}' AND pwd = '${pwd}'
- 正常操作:傳入用戶名
admin,密碼123。 - 攻擊操作:攻擊者在用戶名框輸入
admin' --,密碼隨便寫。 - 最終生成的 SQL:
SELECT * FROM user WHERE username = 'admin' -- ' AND pwd = 'xxx'
在 SQL 中,-- 代表注釋。這意味著 AND pwd = ... 的邏輯被直接注銷掉了!攻擊者無需密碼即可直接以管理員身份登錄。
結(jié)論:絕不能使用 ${} 接收用戶輸入的參數(shù)!
四、 SQL 注入場景演示
控制層:UserTestController
注意自己所寫的類位置以及包
import com.example.demo.model.UserInfo;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserTestController {
@Autowired
private UserTestService userService;
@RequestMapping("/login")
public boolean login(String name, String password) {
UserInfo userInfo = userService.queryUserByPassword(name, password);
if (userInfo != null) {
return true;
}
return false;
}
}業(yè)務(wù)層:UserTestService
import com.example.demo.mapper.UserInfoMapper;
import com.example.demo.model.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserTestService {
@Autowired
private UserInfoTestMapper userInfoMapper;
public UserInfo queryUserByPassword(String name, String password) {
List<UserInfo> userInfos = userInfoMapper.queryUserByPassword(name, password);
if (userInfos != null && userInfos.size() > 0) {
return userInfos.get(0);
}
return null;
}
}數(shù)據(jù)層:UserInfoTestMapper
import com.example.demo.model.UserInfo;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface UserInfoTestMapper {
@Select("select username, `password`, age, gender, phone from user_info where username= '${name}' and password='${password}' ")
List<UserInfo> queryUserByPassword(String name, String password);
}啟動服務(wù),訪問:http://127.0.0.1:8080/login?name=admin&password=admin
程序正常運(yùn)行
接下來訪問SQL注?的代碼:
password設(shè)置為' or 1='1
拼接為:http://127.0.0.1:8080/login?name=admin&password=’ or 1='1
注意看網(wǎng)頁地址?。ò俜痔柡涂崭襁M(jìn)行了URL編碼,因?yàn)閁RL不允許直接添加特殊字符)
五、 ${} 的“唯一”合法使用場景
既然 ${} 這么危險(xiǎn),為什么不廢掉它?因?yàn)樗谔幚?SQL 結(jié)構(gòu)時無可替代:
- 動態(tài)表名:
SELECT * FROM ${tableName}(#{}無法用于表名,因?yàn)轭A(yù)編譯不支持表名占位)。 - 動態(tài)排序字段:
ORDER BY ${column} ${orderType}(如按id或create_time排序,且指定ASC/DESC)。
安全建議:在使用這些場景時,必須在 Service 層做白名單校驗(yàn),確保傳入的列名或表名是合法的。
六、 開發(fā)規(guī)范口訣
為了方便記憶,我們可以總結(jié)為一段口訣:
井號預(yù)編譯安全自帶引號,刀樂拼接危險(xiǎn)手動加引號;
業(yè)務(wù)參數(shù)全用井號,結(jié)構(gòu)關(guān)鍵字才用刀樂。
一句話總結(jié):
平時開發(fā)無腦用 #{};只有在需要動態(tài)傳遞表名、列名、排序關(guān)鍵字且已經(jīng)做好安全過濾的情況下,才考慮使用 ${}。
到此這篇關(guān)于MyBatis中#{}和${}的區(qū)別及底層原理、適用場景和安全風(fēng)險(xiǎn)的文章就介紹到這了,更多相關(guān)MyBatis中#{}和${}的區(qū)別內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Mybatis中#{}和${}傳參的區(qū)別及#和$的區(qū)別小結(jié)
- MyBatis中#{}和${}的區(qū)別詳解
- MyBatis 中 ${}和 #{}的正確使用方法(千萬不要亂用)
- mybatis中${}和#{}的區(qū)別以及底層原理分析
- MyBatis中使用#{}和${}占位符傳遞參數(shù)的各種報(bào)錯信息處理方案
- MyBatis中#{}?和?${}?的區(qū)別和動態(tài)?SQL詳解
- 詳解mybatis #{}和${}的區(qū)別、傳參、基本語法
- MyBatis 探秘之#{} 與 ${} 參傳差異解碼(數(shù)據(jù)庫連接池筑牢數(shù)據(jù)交互根基)
相關(guān)文章
Windows環(huán)境下安裝達(dá)夢數(shù)據(jù)庫的完整步驟
達(dá)夢數(shù)據(jù)庫的安裝大致分為Windows和Linux版本,本文將以dm8 企業(yè)版 Windows_64位 環(huán)境為例,為大家介紹一下達(dá)夢數(shù)據(jù)庫的具體安裝步驟吧2025-03-03
復(fù)制數(shù)據(jù)庫表中兩個字段數(shù)據(jù)的SQL語句
今天為表新添加一個字段,但又想與表中的另一個字段值相同,由于數(shù)據(jù)過多想通過sql語句實(shí)現(xiàn),經(jīng)測試下面的這句話確實(shí)很好用2013-07-07
干掉Navicat,這個數(shù)據(jù)庫管理工具真香
這篇文章主要介紹了干掉Navicat,這個數(shù)據(jù)庫管理工具真香,本文詳細(xì)的介紹DataGrip的具體使用方法和實(shí)現(xiàn),需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10
使用DBeaver連接PostgreSQL實(shí)現(xiàn)方式
DBeaver是一款功能強(qiáng)大的數(shù)據(jù)庫管理工具,支持多種數(shù)據(jù)庫,包括PostgreSQL,以下是使用DBeaver連接PostgreSQL的步驟:打開DBeaver,選擇創(chuàng)建連接,選擇PostgreSQL,輸入服務(wù)器信息,測試連接,最后點(diǎn)擊完成即可2025-11-11
DBeaver轉(zhuǎn)儲數(shù)據(jù)庫報(bào)錯問題解決辦法
DBeaver是一個通用的數(shù)據(jù)庫工具,支持MySQL、PostgreSQL、Oracle、SQLite、SQL Server等多種數(shù)據(jù)庫系統(tǒng),這篇文章主要介紹了DBeaver轉(zhuǎn)儲數(shù)據(jù)庫報(bào)錯問題的解決辦法,需要的朋友可以參考下2025-11-11
檢查數(shù)據(jù)庫服務(wù)器是否正在運(yùn)行的常見方法小結(jié)
在日常的數(shù)據(jù)庫操作和維護(hù)中,確保數(shù)據(jù)庫服務(wù)器正常運(yùn)行是至關(guān)重要的,本文整理了幾種常見的檢查數(shù)據(jù)庫服務(wù)器是否正在運(yùn)行的方法,需要的小伙伴可以了解下2025-04-04

