🌈个人主页:一条泥憨鱼(欢迎各位大佬莅临)
🎬精选专栏:数据结构与算法,Java ,苍穹外卖日记,AI学习,JavaWeb
![]()
前言:
设想一个很有意思的场景:
面试官问:你知道公司为什么禁止拼接SQL吗?
很多人脱口而出:因为会SQL注入
面试官接着问:那你见过真实的SQL注入事故吗,你知道一次SQL注入可能给公司造成多大损失吗?
现场瞬间安静了。
事实上,在很多刚毕业的程序员眼里,SQL注入似乎只是书上的知识点。
但在真实项目中,它可能意味着:
用户数据泄露
订单数据丢失
数据库被删
公司被攻击
老板半夜被电话叫醒
严重一点,第二天你可能就要更新简历了
而这一切,往往只是因为写了这样一行代码:
String sql =
"select * from user where username='"
+ username +
"' and password='"
+ password +
"'";
今天我们就来聊聊:
为什么这种SQL千万别写?
什么是SQL注入?
为什么MyBatis默认不会出现这个问题?
预编译SQL到底厉害在哪里?
看完这篇文章,你不仅能搞懂预编译SQL,还能彻底理解为什么企业开发中几乎没人再手写字符串拼接SQL。
一、这个SQL看起来有什么问题?
很多新手第一次写登录功能时,都是这么干的:
public boolean login(
String username,
String password)
throws Exception {
String sql =
"select * from user " +
"where username='"
+ username +
"' and password='"
+ password +
"'";
Statement stmt =
conn.createStatement();
ResultSet rs =
stmt.executeQuery(sql);
return rs.next();
}
看起来逻辑非常合理。
用户输入:
admin
123456
生成:
select *
from user
where username='admin'
and password='123456'
查询成功–>项目上线–>老板满意,你也满意。
然后第二天出事了。
二、一个神秘用户登录了所有账号
假设有个略懂点数据库的人。
输入用户名:
admin
密码:
' or '1'='1
最终SQL变成:
select *
from user
where username='admin'
and password=''
or '1'='1'
很多同学第一次看到这里,还没意识到问题。
我们拆开看:
'1'='1'
结果是什么?永远成立。
也就是true
于是SQL变成:
条件A
or
true
结果自然是:
true
数据库直接返回数据,攻击者成功登录。
甚至连密码都不需要知道。
三、这就是臭名昭著的SQL注入
这个漏洞有一个专门的名字:
SQL Injection
SQL注入
本质上:
用户把原本应该输入的数据,伪装成SQL代码,让数据库执行了不该执行的语句。
数据库根本不知道:
哪些是程序写的SQL
哪些是用户输入的数据
因为在它看来:
String sql =
"select * from user where id="
+ userInput;
最终就是一段完整SQL。
如果用户输入:
1 or 1=1
数据库看到:
select *
from user
where id=1 or 1=1
然后老老实实执行。
四、为什么大家害怕这种问题?
因为很多人觉得登录绕过而已,没那么严重。
实际上攻击者还能干很多事情。
例如查询所有用户:
1 union select * from user
删除数据:
1; delete from user
甚至删除整个数据库:
drop database company
当然现代数据库会有权限限制,但核心问题仍然存在:
用户修改了SQL结构
这是最危险的地方。
五、解决方案:预编译SQL
企业里解决这个问题的方法只有一个:
预编译SQL
也叫:
PreparedStatement
六、什么叫预编译?
传统拼接SQL:
String sql =
"select * from user where id="
+ id;
数据库拿到的是:
select *
from user
where id=1
或者:
select *
from user
where id=1 or 1=1
数据库根本分不清。
而预编译SQL写法:
String sql =
"select * from user where id=?";
这里: ? 叫占位符。
数据库首先看到:
select *
from user
where id=?
注意,此时:
SQL结构已经固定
数据库先完成:
解析
检查语法
生成执行计划
然后等待参数。
七、第一个PreparedStatement实例
普通写法:
Statement stmt =
conn.createStatement();
String sql =
"select * from user where id="
+ id;
stmt.executeQuery(sql);
预编译写法:
String sql =
"select * from user where id=?";
PreparedStatement ps =
conn.prepareStatement(sql);
ps.setInt(1,1);
ResultSet rs =
ps.executeQuery();
是不是非常简单?
八、参数绑定到底发生了什么?
SQL模板:
select *
from user
where username=?
and password=?
绑定参数:
ps.setString(1,"admin");
ps.setString(2,"123456");
数据库执行时:
select *
from user
where username='admin'
and password='123456'
如果攻击者输入:
' or '1'='1
数据库会把它当成一个普通字符,而不是SQL代码。
最终变成:
password="' or '1'='1"
数据库只会去匹配这个字符串。
不会修改SQL结构。
这就是预编译SQL真正厉害的地方。
九、企业级登录案例
public boolean login(
String username,
String password)
throws Exception {
String sql =
"select * from user " +
"where username=? " +
"and password=?";
PreparedStatement ps =
conn.prepareStatement(sql);
ps.setString(1,username);
ps.setString(2,password);
ResultSet rs =
ps.executeQuery();
return rs.next();
}
十、预编译SQL还有一个隐藏技能
很多人以为它只是安全,其实它还有一个大优势:
性能优化
例如:
for(int i=1;i<=10000;i++){
select * from user where id=i
}
如果使用Statement:
解析SQL
编译SQL
执行SQL
重复10000次。
而PreparedStatement:
编译一次
执行10000次
执行计划直接复用,数据库压力明显下降。
十一、为什么MyBatis默认防SQL注入?
很多同学学MyBatis时,经常写:
<select id="getUser">
select *
from user
where id = #{id}
</select>
这里的#{id},实际上底层就是:?
最终调用PreparedStatement。所以#{} 默认安全
十二、${}为什么容易出问题?
例如:
select *
from user
where id=${id}
用户输入:
1 or 1=1
最终:
select *
from user
where id=1 or 1=1
又回到了字符串拼接时代,因此规范通常要求:
能用 #{},绝不用 ${}
面试官最爱问的几个问题
什么是预编译SQL?
SQL结构先固定。
参数后传递。
用户无法修改SQL结构。
PreparedStatement为什么能防SQL注入?
因为SQL结构和用户数据彻底分离
PreparedStatement为什么性能更高?
执行计划复用,减少重复编译。
#{}和${}区别?
#{} → 预编译
${} → 字符串拼接
总结
SQL注入并不可怕,可怕的是你明明知道有风险,还在项目里拼接SQL。
在开发中:
❌ 不要这样写:
String sql =
"select * from user where id="
+ id;
✅ 要这样写:
PreparedStatement
或者:
#{}
因为预编译SQL不仅能防SQL注入,还能提升性能
这也是为什么几乎所有现代ORM框架底层都在使用预编译SQL。
下次当你写SQL的时候,记住一句话:
你拼接的可能不是SQL,而是老板明天找你谈话的理由。

