从 01 开始 从 01 开始
首页
  • 📚 计算机基础

    • 计算机简史
    • 数字电路
    • 计算机组成原理
    • 操作系统
    • Linux
    • 计算机网络
    • 数据库
    • 编程工具
    • 装机
  • 🎨 前端

    • Node
  • JavaSE
  • Java 高级
  • JavaEE

    • 构建、依赖管理
    • Ant
    • Maven
    • 日志框架
    • Junit
    • JDBC
    • XML-JSON
  • JavaWeb

    • 服务器软件
    • 环境管理和配置管理-科普篇
    • Servlet
  • Spring

    • Spring基础
  • 主流框架

    • Redis
    • Mybatis
    • Lucene
    • Elasticsearch
    • RabbitMQ
    • MyCat
    • Lombok
  • SpringMVC

    • SpringMVC 基础
  • SpringBoot

    • SpringBoot 基础
  • Windows 使用技巧
  • 手机相关技巧
  • 最全面的输入法教程
  • 最全面的浏览器教程
  • Office
  • 图片类工具
  • 效率类工具
  • 最全面的 RSS 教程
  • 码字工具
  • 各大平台
  • 校招
  • 五险一金
  • 职场规划
  • 关于离职
  • 杂谈
  • 自媒体
  • 📖 读书

    • 读书工具
    • 走进科学
  • 🌍 英语

    • 从零开始学英语
    • 英语兔的相关视频
    • Larry 想做技术大佬的相关视频
  • 🏛️ 政治

    • 新闻合订本
    • 反腐
    • GFW
    • 404 内容
    • 审查与自我审查
    • 互联网
    • 战争
    • 读书笔记
  • 💰 经济

    • 关于税
    • 理财
  • 💪 健身

    • 睡眠
    • 皮肤
    • 口腔健康
    • 学会呼吸
    • 健身日志
  • 🏠 其他

    • 驾驶技能
    • 租房与买房
    • 厨艺
  • 电影

    • 电影推荐
  • 电视剧
  • 漫画

    • 漫画软件
    • 漫画推荐
  • 游戏

    • Steam
    • 三国杀
    • 求生之路
  • 小说
  • 关于本站
  • 关于博主
  • 打赏
  • 网站动态
  • 友人帐
  • 从零开始搭建博客
  • 搭建邮件服务器
  • 本站分享
  • 🌈 生活

    • 2022
    • 2023
    • 2024
    • 2025
  • 📇 文章索引

    • 文章分类
    • 文章归档

晓林

程序猿,自由职业者,博主,英语爱好者,健身达人
首页
  • 📚 计算机基础

    • 计算机简史
    • 数字电路
    • 计算机组成原理
    • 操作系统
    • Linux
    • 计算机网络
    • 数据库
    • 编程工具
    • 装机
  • 🎨 前端

    • Node
  • JavaSE
  • Java 高级
  • JavaEE

    • 构建、依赖管理
    • Ant
    • Maven
    • 日志框架
    • Junit
    • JDBC
    • XML-JSON
  • JavaWeb

    • 服务器软件
    • 环境管理和配置管理-科普篇
    • Servlet
  • Spring

    • Spring基础
  • 主流框架

    • Redis
    • Mybatis
    • Lucene
    • Elasticsearch
    • RabbitMQ
    • MyCat
    • Lombok
  • SpringMVC

    • SpringMVC 基础
  • SpringBoot

    • SpringBoot 基础
  • Windows 使用技巧
  • 手机相关技巧
  • 最全面的输入法教程
  • 最全面的浏览器教程
  • Office
  • 图片类工具
  • 效率类工具
  • 最全面的 RSS 教程
  • 码字工具
  • 各大平台
  • 校招
  • 五险一金
  • 职场规划
  • 关于离职
  • 杂谈
  • 自媒体
  • 📖 读书

    • 读书工具
    • 走进科学
  • 🌍 英语

    • 从零开始学英语
    • 英语兔的相关视频
    • Larry 想做技术大佬的相关视频
  • 🏛️ 政治

    • 新闻合订本
    • 反腐
    • GFW
    • 404 内容
    • 审查与自我审查
    • 互联网
    • 战争
    • 读书笔记
  • 💰 经济

    • 关于税
    • 理财
  • 💪 健身

    • 睡眠
    • 皮肤
    • 口腔健康
    • 学会呼吸
    • 健身日志
  • 🏠 其他

    • 驾驶技能
    • 租房与买房
    • 厨艺
  • 电影

    • 电影推荐
  • 电视剧
  • 漫画

    • 漫画软件
    • 漫画推荐
  • 游戏

    • Steam
    • 三国杀
    • 求生之路
  • 小说
  • 关于本站
  • 关于博主
  • 打赏
  • 网站动态
  • 友人帐
  • 从零开始搭建博客
  • 搭建邮件服务器
  • 本站分享
  • 🌈 生活

    • 2022
    • 2023
    • 2024
    • 2025
  • 📇 文章索引

    • 文章分类
    • 文章归档
  • JavaSE

  • JavaSenior

  • JavaEE

    • JavaEE 概念

    • Ant

    • Maven

    • 日志

    • Junit

    • JDBC

      • 什么是 JDBC
      • JDBC 连接、查询和更新
      • JDBC 连接字符串
      • JDBC 常用类介绍
      • PreparedStatement 是如何防止SQL注入的
        • 数据准备
        • 一个简单的登录逻辑
        • 使用 Statement 演示 SQL 注入
        • 使用 PreparedStatement 演示 SQL 注入
        • PreparedStatement 的源码
        • 总结
      • ResultSet 结果封装为对象
      • JDBC 工具类
      • JDBC 更新
      • JDBC 事务和批量
      • JDBC 连接池
      • JDBCTemplate
    • XML 和 JSON

  • JavaWeb

  • Spring

  • 主流框架

  • SpringMVC

  • SpringBoot

  • Java
  • JavaEE
  • JDBC
2023-03-24
目录

PreparedStatement 是如何防止SQL注入的

# 5.PreparedStatement 是如何防止 SQL 注入的

# 数据准备

为了方便演示,我们创建一个 user 表来模拟登录的情况

USE learnjdbc;

CREATE TABLE user (
  id BIGINT AUTO_INCREMENT NOT NULL,
  name VARCHAR(50) NOT NULL,
  password VARCHAR(50) NOT NULL,
  PRIMARY KEY(id)
) Engine=INNODB DEFAULT CHARSET=UTF8;

INSERT INTO user (id, name, password) VALUES (1, 'peterjxl', '123456')
INSERT INTO user (id, name, password) VALUES (2, 'peter', '123456')
1
2
3
4
5
6
7
8
9
10
11

# 一个简单的登录逻辑

这里也说明下登录的逻辑:

  1. 用户输入用户名和密码
  2. Java 程序收到,并执行 SQL 判断用户名密码是否存在
  3. 如果一致则认为通过 ‍ SQL 逻辑如下:
select count(1) from user where name = '用户名' and password = '密码'
1

‍ 如果用户输入的是正确的用户名和密码(例如 peterjxl 和 123456),则执行的 SQL 是:

select count(1) from user where name = 'peterjxl' and password = '123456'
1

执行结果是 1,说明存在数据,并且用户名和密码都是正确的,认为通过了用户密码校验,登录成功。

相关代码如下:这里假设 name 和 password 是用户输入的变量。

String name = "peterjxl";
String password = "123456";
Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
Statement statement = conn.createStatement();
ResultSet rs = statement.executeQuery("select count(1) from user where name ='" + name + "' and password = '" + password + "'");
while (rs.next()){
    if (0 != rs.getInt(1)){
        System.out.println("登录成功");
    }else{
        System.out.println("登录失败");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 使用 Statement 演示 SQL 注入

如果用户输入的是一个特殊的字符呢?

String name = "peterjxl";
String password = "1' or ''='";
Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
Statement statement = conn.createStatement();
ResultSet rs = statement.executeQuery("select count(1) from user where name ='" + name + "' and password = '" + password + "'");
while (rs.next()){
    if (0 != rs.getInt(1)){
        System.out.println("登录成功");
    }else{
        System.out.println("登录失败");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

以上代码拼接 SQL 后,如下:

select count(1) from user where name = 'peterjxl' and PASSWORD = '1' or ''=''
1

也就是说,即使用户不知道密码,只要使用 SQL 注入,就可以登录成功。

更糟糕的情况是,如果用户直接输入了一些删除表和数据库的语句,一旦被执行,后果不堪设想!例如他们可以这样拼接一大串字符:

select count(1) from user where name = 'peterjxl' and PASSWORD = '1' or ''='';
delete from user;
select count(1) from user where ''=''
1
2
3

# 使用 PreparedStatement 演示 SQL 注入

如果我们使用的是 PreparedStatement 呢?能否 SQL 注入?我们来试试看:为了方便,第 8 行我们打印执行的 SQL

String name = "peterjxl";
String password = "1' or ''='";
Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD);
PreparedStatement stapreparedStatementement = conn.prepareStatement("select count(1) from user where name = ? and password = ?");
stapreparedStatementement.setObject(1, name); 
stapreparedStatementement.setObject(2, password); 
ResultSet rs = stapreparedStatementement.executeQuery();
System.out.println("sql = " + stapreparedStatementement.toString());
while (rs.next()){
    if (0 != rs.getInt(1)){
        System.out.println("登录成功");
    }else{
        System.out.println("登录失败");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

运行结果如下:

sql = com.mysql.cj.jdbc.ClientPreparedStatement: select count(1) from user where name = 'peterjxl' and password = '1'' or ''''='''
登录失败
1
2

‍ 为什么 PreparedStatement 能防止 SQL 注入?因为它会把用户输入的单引号做转义,从而防止 SQL 注入。

# PreparedStatement 的源码

PreparedStatement 只是一个接口,具体的实现类得看数据库厂商实现的源码是怎么样的,这里以 MySQL 驱动为例,观察其源码。

如果你使用 IDE,可以直接在 IDE 里查看源码,这里以 IDEA 为例:

‍ 然后我们可以打断点调试,注意 fillSendPacket 方法,将 sql 转换为字节流的过程中,给占位符中包含的单引号',额外加上一个单引号。

‍

接下来我们继续 debug,到底是哪个操作加上了单引号:

找到 buligComQuery()方法,有如下代码,其中:

‍

  1. staticSqlStrings[] :存储的是以占位符 ? 划分开的 sql 语句转换的字节数组,不包含参数,即
  • staticSqlStrings[0] = SELECT * FROM staff_info a WHERE a.staff_age =
  • staticSqlStrings[1] = and a.staff_sex =
  1. bindValues[] :绑定的参数数组

从以上源码可以看出,sql 语句和参数共同组装成 sendPacket 发给远程数据库做执行,而防 SQL 注入的处理,肯定就在 writeAsText() 流程中;

我们继续往下,来到 com.mysql.cj.protocol.a.StringValueEncoder 类中,该类用于处理 String 参数,于是可以定位到在将参数转换为字节数组的 getBytes() 方法中对输入的字符串进行了 StringUtils.escapeString() 处理:

既然已经发现字符串的工具类调用,不出意外我们马上就要找到答案了

可以看到方法中对各类特殊字符都做了转义处理,常规的添加斜杠 \ 如换行符\n ;而我们这里涉及的单引号 ' ,转义时会再添加一个 ', 这也解释了上述最终执行 sql 的生成。

注意,不同版本的数据库驱动,代码可能有点不同:

  • MySQL 5 的驱动是加上反斜杠 \ 做转义;
  • MySQL 8 的驱动是给单引号 额外加上一个单引号做转义

但无论如何实现,都能防止 SQL 注入。

不同版本的 MySQL 驱动,都是开源的,读者可以去官网下载:MySQL :: MySQL Connector/J 8.0 Developer Guide :: 4.3 Installing from Source (opens new window)

# 总结

生产环境一定要用 PreparedStatement,而不是用 Statement!

参考:

java 中 PreparedStatement 和 Statement 详细讲解_程宇寒的博客-CSDN 博客 (opens new window):本文主要参考了这位大佬的博客,并且分析了 MySQL5 驱动中 PreparedStatement 防止 SQL 注入的源码

PrepareStatement 用法(附源码解析)_preparestatement 的用法_DunkingCurry 的博客-CSDN 博客 (opens new window):分析了 MySQL 8 驱动中 PreparedStatement 防止 SQL 注入的源码

上次更新: 2025/6/3 09:31:54
JDBC 常用类介绍
ResultSet 结果封装为对象

← JDBC 常用类介绍 ResultSet 结果封装为对象→

最近更新
01
新闻合订本 2025-10
10-31
02
2025 年 10 月记
10-30
03
用 AI 批量优化思源笔记排版
10-15
更多文章>
Theme by Vdoing | Copyright © 2022-2025 | 粤 ICP 备 2022067627 号 -1 | 粤公网安备 44011302003646 号 | 点击查看十年之约
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式