面向小白,讲清楚"什么是 SQL 注入、为什么发生、如何防御",并给出基于 Java/MyBatis 的实战代码。所有关键代码都带注释,方便直接参考。
1. 什么是 SQL 注入?
- 定义:攻击者把恶意的 SQL 片段当作"输入"注入到应用程序拼接的 SQL 里,让数据库执行原本不该执行的操作。
- 危害:绕过登录校验、越权查询/修改/删除数据、批量拖库、写入后门。
- 本质原因:开发者将用户输入直接拼接到 SQL 字符串,而没有使用预编译或有效校验。
2. 经典注入示例
登录绕过
原始(错误)拼接代码:
sql
select * from user where username = 'admin' and password = '' or '1'='1'
攻击者在密码输入 ' or '1'='1,条件恒真,直接登录成功。
模糊查询注入
错误写法(字符串直拼):
xml
<select id="likeByName" resultType="entity.User">
select * from user where name like '%${value}%'
</select>
如果输入 a%' or '1'='1,SQL 变成:
sql
select * from user where name like '%a%' or '1'='1%'
3. 防御核心思路
- 预编译参数化 :用占位符
?(JDBCPreparedStatement)或 MyBatis 的#{},让驱动处理转义和类型绑定。 - 避免字符串直拼 :不要用
${}直接拼接用户输入;动态表名/列名必须先白名单校验再拼。 - 输入校验/长度限制:对外部输入做类型、长度、格式校验,阻断畸形 payload。
- 最小权限:数据库账号只授予必须权限,减少被注入后的危害面。
- 审计与日志:开启 SQL 日志,便于发现异常访问模式。
4. Java/JDBC 不安全 vs 安全示例
不安全(字符串拼接)
java
String sql = "select * from user where name like '%" + keyword + "%'"; // ❌
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
安全(PreparedStatement 预编译)
java
String sql = "select * from user where name like ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, "%" + keyword + "%"); // 参数绑定,驱动负责转义
ResultSet rs = ps.executeQuery();
说明:
?占位符 +setXxx绑定,避免直接拼接。- 任何用户输入都走绑定流程,特殊字符会被安全处理。
5. MyBatis 防注入示例
5.1 安全的模糊查询(推荐)
Mapper XML:
xml
<select id="likeByNameSafe" resultType="entity.User" parameterType="java.lang.String">
select * from user where name like concat('%', #{value}, '%') <!-- #{ } 预编译 -->
</select>
接口:
java
List<User> likeByNameSafe(String name);
5.2 不安全写法对比(仅示例,不要用)
xml
<select id="likeByNameUnsafe" resultType="entity.User" parameterType="java.lang.String">
select * from user where name like '%${value}%' <!-- ${ } 直接拼接,易注入 -->
</select>
5.3 多条件动态查询(使用 where + if,仍是安全的 #{})
xml
<select id="dtFindUser" parameterType="entity.User" resultType="entity.User">
select * from user
<where>
<if test="name != null"> and name like concat('%', #{name}, '%') </if>
<if test="gender != null"> and gender = #{gender} </if>
<if test="phone != null"> and phone like concat('%', #{phone}, '%') </if>
</where>
</select>
要点:所有用户输入都在 #{} 中,依然是预编译绑定。
5.4 批量 IN 查询/删除(foreach + #{})
xml
<delete id="deleteMore">
delete from user where id in
<foreach item="id" collection="ids" open="(" close=")" separator=",">
#{id} <!-- 每个元素单独绑定,防注入 -->
</foreach>
</delete>
6. 动态表名/列名的安全做法(白名单)
若业务必须动态指定列/表名,不能用 #{},只能字符串拼接,但要白名单校验:
java
// 例:只允许按特定列排序
private static final Set<String> ALLOWED_ORDER = Set.of("name", "age", "birthday");
public List<User> listOrderBy(String col) {
if (!ALLOWED_ORDER.contains(col)) {
throw new IllegalArgumentException("bad order column");
}
// 安全拼接通过白名单后的列名,值仍用 #{} 绑定
String sql = "select * from user order by " + col + " asc";
// 执行...
}
在 MyBatis XML 中,同理可写:
xml
<select id="listOrderBySafe" resultType="entity.User">
select * from user order by ${col} <!-- col 必须在 Java 侧白名单校验 -->
</select>
7. 输入校验与长度限制示例
java
void checkKeyword(String keyword) {
if (keyword == null || keyword.length() > 50) {
throw new IllegalArgumentException("keyword too long");
}
// 可选:正则仅允许中文/英文/数字/空格
if (!keyword.matches("[\\p{L}\\p{N} ]+")) {
throw new IllegalArgumentException("keyword invalid");
}
}
在调用查询前先校验,再传给 Mapper。
8. 数据库侧最小权限配置
- 生产环境不要用 root;为应用创建专用账号,只授予需要的库/表及 CRUD 权限。
- 禁止授予
FILE、SUPER等高危权限,避免被注入后写入系统文件或执行管理操作。
9. 日志与审计
- 在 MyBatis 配置里打开 STDOUT 或接入日志框架:
xml
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
- 结合数据库审计(慢查询、异常查询)发现异常模式。
10. 结合本项目的小结实践
- 模糊查询:用
like concat('%', #{value}, '%'),不要用%${value}%。 - 批量操作:用
<foreach>#{id}</foreach>绑定每个元素。 - 动态 SQL:
<if>/<where>/<set>/<trim>/<choose>/<foreach>里统一用#{}绑定。 - 必要的动态列/表:先做白名单,再用
${}。
11. 常见面试题与简答
-
什么是 SQL 注入?如何防御?
- 把恶意 SQL 当输入拼进语句并执行;防御靠预编译参数化(
#{}/?)、输入校验、最小权限、禁用${}直拼。
- 把恶意 SQL 当输入拼进语句并执行;防御靠预编译参数化(
-
MyBatis 中
#{}与${}区别?#{}预编译占位,安全防注入;${}字符串直接替换,易注入,仅用于白名单后的动态对象名。
-
为什么
PreparedStatement能防注入?- SQL 先编译,参数后绑定,参数不会被当作 SQL 结构解析,特殊字符会被转义。
-
如何安全做模糊查询?
- SQL 用
like concat('%', #{kw}, '%');不要把%拼在 SQL 字符串里与用户输入直接混合。
- SQL 用
-
动态表名/列名怎么办?
- 只能用
${}拼接,但必须先在 Java 做白名单校验,只允许固定集合内的值。
- 只能用
-
批量 IN 防注入怎么做?
<foreach>+#{}绑定每个元素;不要拼成"in (" + ids + ")"。
-
数据库层如何降低风险?
- 最小权限账户、限制高危权限、配合审计和日志。
-
有哪些迹象可能是注入攻击?
- 出现总为真的条件(
1=1)、异常的 OR 拼接、Union 关键字、批量查询系统表、频繁报错信息探测等。
- 出现总为真的条件(
12. 练习建议
- 把项目中的
${value}写法改成#{}(如likeByName2),对比生成 SQL 与执行效果。 - 为动态排序/列名加上 Java 侧白名单校验。
- 打开 SQL 日志,尝试构造恶意输入,看是否被拦截。
- 检查数据库账号权限,确保非 root,权限最小化。
13. 结语
SQL 注入的核心防线很简单:参数化 + 校验 + 白名单 + 最小权限。只要坚持这几条,绝大多数注入风险都能被消灭在"输入"阶段。祝学习顺利,面试稳稳拿下! 😊