这么写SQL语句,老板让我明天不用来了!
🌈个人主页:一条泥憨鱼(欢迎各位大佬莅临)
🎬精选专栏:数据结构与算法,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,而是老板明天找你谈话的理由。
