PrepareStatement用法(附源码解析)

PrepareStatement 基本用法

1. 加载驱动

首先在pom.xml 中引入 mysql 依赖

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.23</version>
</dependency>

通过以下代码加载驱动

Class.forName("com.mysql.cj.jdbc.Driver");

2. 获取 Connection 对象

数据库连接需要数据库链接、用户名、密码三个参数,建议配置在application.properties 中,方便统一管理(当然也可以直接在使用的地方用字符串变量)

#PrepareStatement
#数据库链接
pstmt.dbUrl="jdbc:mysql://localhost:3306/mydb?serverTimezone=UTC&characterEncoding=utf8&useUnicode=true"
#用户名
pstmt.dbUser=root
#密码
pstmt.dbPwd=123456

通过以下代码获取connection

Connection connection = DriverManager.getConnection(dbUrl,dbUser,dbPwd);

3. 获取 PrepareStatement 对象

根据需求创建Sql语句字符串,在需要加入参数的地方用 ? 占位符代替

//查询符合指定年龄和性别的员工姓名
String sql = "SELECT a.name FROM staff_info a WHERE a.age = ? and a.sex = ?";

接下来,创建 PrepareStatement对象对sql进行预编译

PreparedStatement pstmt = connection.prepareStatement(sql);

4. 设置参数

PrepareStatement对象提供多种参数类型方法,使用时依据数据库字段类型一一对应,设置时需注意第一个参数为占位符? 的索引且从 1 开始,例如之前sql的设置应为

pstmt.setInt(1,35);
pstmt.setString(2,"男");

5. SQL执行

单条语句执行

//单次查询
pstmt.executeQuery();
//单次更新
pstmt.executeUpdate();

批量执行

若一次要插入大量数据,应尽量减少jdbc的调用次数,使用批量执行来优化

//第一批
pstmt.setInt(1,35);
pstmt.setString(2,"男");
//加入批次
pstmt.addBatch();

//第二批
pstmt.setInt(1,24);
pstmt.setString(2,"女");
pstmt.addBatch();

//统一批量执行
pstmt.executeBatch();

6. 完整示例

@Component
public class PrepareStatementUtils {
    private  final Logger LOGGER = LoggerFactory.getLogger(PrepareStatementUtils.class);

    @Value("${pstmt.dbUrl}")
    private  String dbUrl;

    @Value("${pstmt.dbUser}")
    private  String dbUser;

    @Value("${pstmt.dbPwd}")
    private  String dbPwd;

    public void prepareStatementExecute() {
        try {
            //加载 mysql 驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
            //获取链接
            Connection connection = DriverManager.getConnection(dbUrl,dbUser,dbPwd);
            //获取preparestatement对象
            String sql = "SELECT a.name FROM staff_info a WHERE a.age = ? and a.sex = ?";
            PreparedStatement pstmt = connection.prepareStatement(sql);

            //单次执行
            pstmt.setInt(1,35);
            pstmt.setString(2,"男");
            ResultSet rs = pstmt.executeQuery();

            //批量执行
            String sql2 = "insert into staff_info(staff_id,staff_name,staff_age,staff_sex) values(?,?,?,?)";
            PreparedStatement pstmt1 = connection.prepareStatement(sql2);
            //第一批
            pstmt1.setString(1,"001");
            pstmt1.setString(2,"小谭");
            pstmt1.setInt(3,35);
            pstmt1.setString(4,"男");
            //加入批次
            pstmt1.addBatch();

            //第二批
            pstmt1.setString(1,"002");
            pstmt1.setString(2,"小高");
            pstmt1.setInt(3,24);
            pstmt1.setString(4,"女");
            pstmt1.addBatch();

            //统一执行
            pstmt1.executeBatch();

        } catch (ClassNotFoundException | SQLException e) {
            LOGGER.error(e.getMessage());
        }
    }
}

SQL注入攻击(附源码解析)

SQL注入攻击: 通过将恶意的 Sql 查询或添加语句插入到应用的输入参数中,再在后台 Sql 服务器上解析执行进行的攻击,它是目前黑客对数据库进行攻击的最常用手段之一。

1. Statement 发生 SQL 注入的情况

现存在以下语句查询指定年龄的职工信息,其中age为入参

String sql = "SELECT * FROM staff_info a WHERE a.staff_age ="+age;
ResultSet rs = statement.executeQuery(sql);

若age值为24 or 1=1

String age = "24 or 1=1";

最后的拼接语句为

SELECT * FROM staff_info a WHERE a.staff_age = 36 OR 1=1;

会返回该表的所有职工信息;常见的SQL注入还存在于恶意删表,例如:

String age = "24; drop table staff_info";

该语句会删除职工信息表,造成严重后果,因此防止SQL注入是必备操作。

2. PrepareStatement 防止SQL注入源码解析

同样存在以下语句查询指定年龄和性别的职工信息,其中age和sex为入参

String sql = "SELECT * FROM staff_info a WHERE a.staff_age = ? and a.staff_sex = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);

同样以SQL注入的形式设置sex参数为 male' or 1='1

pstmt.setInt(1,36); 
pstmt.setString(2,"male' or 1='1");

打印一下看看PrepareStatement预编译的语句,

System.out.println(pstmt);

从以下结果可知,PrepareStatement会在占位符的两侧加上 ' ,但你可能已经发现到目前为止预编译拼接的sql仍然是存在SQL注入风险的sql,若被直接执行,也会返回表的全部信息,别着急,我们接着看;

接下来执行SQL;

ResultSet rspre = pstmt.executeQuery();

通过 debug 进入 com.mysql.cj.jdbc.ClientPreparedStatementexecuteQuery()方法

可以看出,在 fillSendPacket() 方法将sql转换为字节的过程中,给占位符中包含的单引号',额外加上一个单引号 ' ,最后执行的sql就变为了:

SELECT * FROM staff_info a WHERE a.staff_age =  36 and a.staff_sex = 'male'' or 1=''1'

该查询返回信息为空,避免了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 的生成。

Ps:最近干活用到PrepareStatement,网上提到的PrepareStatement源码解析多为老驱动(做法为添加斜杠\ ),实际使用时发现转义后其实添加的是单引号' ,正好就顺道学习下源码,也借此回顾一下SQL注入。

欢迎访问个人博客:DunkingCurry’ s Blog

  • 7
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值