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 小时前
为何通用堡垒机无法在数据库运维中实现精准风控?
数据库·sql·安全·低代码·架构
枷锁—sha8 小时前
【SRC】SQL注入快速判定与应对策略(一)
网络·数据库·sql·安全·网络安全·系统安全
独断万古他化9 小时前
【SSM开发实战:博客系统】(三)核心业务功能开发与安全加密实现
spring boot·spring·mybatis·博客系统·加密
怣5011 小时前
MySQL多表连接:全外连接、交叉连接与结果集合并详解
数据库·sql
证榜样呀12 小时前
2026 中专大数据技术专业可考的证书有哪些,必看!
大数据·sql
Codefengfeng13 小时前
数据安全知识点速通
sql
fengxin_rou13 小时前
[Redis从零到精通|第四篇]:缓存穿透、雪崩、击穿
java·redis·缓存·mybatis·idea·多线程
逍遥德14 小时前
Sring事务详解之02.如何使用编程式事务?
java·服务器·数据库·后端·sql·spring
驾数者14 小时前
Flink SQL实时数仓实战:基于Flink SQL的完整项目案例
sql·flink·linq