PreparedStatement 是一个接口,它继承自 Statement,用于预编译 SQL 语句。简单来说,PreparedStatement 是 JDBC 提供的一个对象,用于执行 SQL 语句。它的重要功能是帮助防止 SQL 注入攻击,并提高执行效率。
SQL 注入问题
以用户登录为例,通常用户需要先注册一个用户名,并且为这个用户名设置一个对应的密码,通过正确的用户名和匹配的密码才能登录系统。如果用户名不存在或者密码有误,则无法登录系统。然而,SQL 注入则能“无视”这个密码检查的过程,通过一段特定的密码文本,就能在用户名不存在或密码错误的情况下仍然顺利登录系统。
例如,在一个安全新不够高的系统中,通过 ' or '1' = '1
这样一段文本就能成功地登录该系统,即便用户名不存在或这段文本并非匹配的密码。这就是 SQL 注入的一个实例。这个简单实例背后的原理是:在后端进行账户信息检查时,会利用前端传来的用户名和密码到数据库中查询是否存在满足条件的行,过程与下面的代码类似:
String sql = "SELECT * FROM users WHERE username = '" + name + "' and password = '" + pwd + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);if (rs.next()) {// 执行登录成功后的逻辑System.out.println("登录成功");
} else {// 执行登录失败后的逻辑System.out.println("登录失败");
}
在这个过程中,我们使用了 JDBC 中常用的 Statement 对象执行 SQL 语句。乍一看,后端拼接的 SQL 语句没有什么问题,但是,用前文提及的 ' or '1' = '1
替换密码 pwd
后,就会发现问题。假设这里输入的用户名为 a
,拼接后的 SQL 语句为:
SELECT * FROM users WHERE username = 'a' and password = '' or '1' = '1'
对 SQL 语句进行分析可以知道,username = 'a' and password = ''
先执行,不管这句的结果如何,or
后面的部分恒成立,即无论输入的用户名和密码是什么,都能执行成功并且返回所有数据。因此,用户可以通过 SQL 注入随意登录系统。为了防止 SQL 注入的发生,Java 推出了 PreparedStatement。
SQL 注入防范
我们使用 PreparedStatement 预防 SQL 注入,具体过程为:
(1)获取 PreparedStatement 对象
// 使用 `?` 占位符替代 SQL 语句中的参数值
String sql = "SELECT * FROM users WHERE username = ? and password = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
(2)设置参数值
// 设置参数值,根据索引赋值,索引从 1 开始计数
pstmt.setString(1, name);
pstmt.setString(2, pwd);
(3)执行 SQL 语句
// 执行 SQL 语句,不需要再传入 sql
ResultSet rs = pstmt.executeQuery();
这个过程中,解决 SQL 注入的原理是:将传入参数中包含的敏感字符进行转移,比如对单引号进行转义,将参数中的'
转义成 \'
,以此避免与 SQL 语句中的 '
混淆。
预编译原理
使用 PreparedStatement 除了能通过将敏感字符进行转移来防止 SQL 注入外,还可以预编译 SQL,提高性能。
通常,Java 代码将 SQL 语句发送给 MySQL 服务器后,服务器会检查 SQL 语法,再编译 SQL,最后执行 SQL,如上图所示。前两者花费的时间比后者大。
在使用 Statement 执行拼接得到的 SQL 语句时,每次都需要获取一个拼接好的 SQL 语句,并发送给 MySQL 服务器,这时服务器都需要执行上述的三个步骤,效率较低。而预编译语句则不同,使用占位符生成 SQL 模板,并且将模板先发送给 MySQL 服务器,这时服务器会直接执行检查 SQL 语法和编译 SQL。在 SQL 模板不变的情况下,当有新的参数传递过来时,只需要用参数替换占位符并且跳过前两个步骤直接执行 SQL 即可,效率有所提升。需要注意的是,预编译功能需要通过将 url 中的参数 useServerPrepStmts
设置为 true
来开启:
String url = "jdbc:mysql://127.0.0.1:3306/db?useSSL=false&useServerPrepStmts=true";