SQL 注入指南

面向小白,讲清楚"什么是 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. 防御核心思路

  1. 预编译参数化 :用占位符 ?(JDBC PreparedStatement)或 MyBatis 的 #{},让驱动处理转义和类型绑定。
  2. 避免字符串直拼 :不要用 ${} 直接拼接用户输入;动态表名/列名必须先白名单校验再拼。
  3. 输入校验/长度限制:对外部输入做类型、长度、格式校验,阻断畸形 payload。
  4. 最小权限:数据库账号只授予必须权限,减少被注入后的危害面。
  5. 审计与日志:开启 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 权限。
  • 禁止授予 FILESUPER 等高危权限,避免被注入后写入系统文件或执行管理操作。

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. 常见面试题与简答

  1. 什么是 SQL 注入?如何防御?

    • 把恶意 SQL 当输入拼进语句并执行;防御靠预编译参数化(#{} / ?)、输入校验、最小权限、禁用 ${} 直拼。
  2. MyBatis 中 #{}${} 区别?

    • #{} 预编译占位,安全防注入;${} 字符串直接替换,易注入,仅用于白名单后的动态对象名。
  3. 为什么 PreparedStatement 能防注入?

    • SQL 先编译,参数后绑定,参数不会被当作 SQL 结构解析,特殊字符会被转义。
  4. 如何安全做模糊查询?

    • SQL 用 like concat('%', #{kw}, '%');不要把 % 拼在 SQL 字符串里与用户输入直接混合。
  5. 动态表名/列名怎么办?

    • 只能用 ${} 拼接,但必须先在 Java 做白名单校验,只允许固定集合内的值。
  6. 批量 IN 防注入怎么做?

    • <foreach> + #{} 绑定每个元素;不要拼成 "in (" + ids + ")"
  7. 数据库层如何降低风险?

    • 最小权限账户、限制高危权限、配合审计和日志。
  8. 有哪些迹象可能是注入攻击?

    • 出现总为真的条件(1=1)、异常的 OR 拼接、Union 关键字、批量查询系统表、频繁报错信息探测等。

12. 练习建议

  • 把项目中的 ${value} 写法改成 #{}(如 likeByName2),对比生成 SQL 与执行效果。
  • 为动态排序/列名加上 Java 侧白名单校验。
  • 打开 SQL 日志,尝试构造恶意输入,看是否被拦截。
  • 检查数据库账号权限,确保非 root,权限最小化。

13. 结语

SQL 注入的核心防线很简单:参数化 + 校验 + 白名单 + 最小权限。只要坚持这几条,绝大多数注入风险都能被消灭在"输入"阶段。祝学习顺利,面试稳稳拿下! 😊

相关推荐
码农水水6 小时前
小红书Java面试被问:SQL语句的执行过程解析
数据库·sql
youngqqcn6 小时前
SQL中联表查询深入分析
数据库·sql
Java&Develop6 小时前
PL/SQL Developer可视化修改数据
数据库·sql
武昌库里写JAVA7 小时前
vue+iview+node+express实现文件上传,显示上传进度条,实时计算上传速度
java·vue.js·spring boot·后端·sql
码灵7 小时前
Mysql sql查询优化
sql
码灵8 小时前
SQL 关键字汇总
数据库·sql
lightningyang9 小时前
渗透入门之SQL 注入(1)
数据库·sql·渗透·sql注入·天枢一体化虚拟仿真平台
爱可生开源社区9 小时前
在数据库迁移中,如何让 AI 真正“可用、可信、可落地”?
数据库·sql·llm
山峰哥9 小时前
SQL性能优化实战:从索引策略到查询优化案例全解析
大数据·数据库·sql·oracle·性能优化·架构