SQL注入问题及其解决办法
前几次的 JDBC 案列的数据库操作对象我一直使用的是 Statement 对象, 但是这个类实际上是存在一些问题的, 这就是我想要分享的 SQL 注入问题.
目录
PreparedStatement 和 Statement 比较
问题发现
前几天用 JDBC 编写一个从数据库获取用户名和密码登录的简单 java 程序发现了一个 bug
下面请看源代码:
// 连接数据库获取账号和密码登录
public class Test06 {
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
PreparedStatement ps = null;
try {
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=true";
String username = "root";
String password = "XXXX";
conn = DriverManager.getConnection(url, username, password);
Scanner scanner = new Scanner(System.in);
System.out.print("请输入用户名: ");
String un = scanner.nextLine();
System.out.print("请输入密 码: ");
String pw = scanner.nextLine();
// 下面代码存在 sql 注入问题
String sql = "select * from user where name = '" + un
+ "' and password = '" + pw + "'";
stmt = conn.createStatement();
// 下面这个代码的含义为: 将sql语句给DBMS, DBMS进行编译
// 是先提交后编译, 所以如果用户提供了非法信息, 就导致了 sql 语句含义被扭曲
// 从而出现不符合用户需求的情况
rs = stmt.executeQuery(sql);
if (rs.next()) {
System.out.println("登录成功");
}else {
System.out.println("登录失败");
}
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
} finally {
try {
if (rs != null) {
rs.close();
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
try {
if (stmt != null) {
stmt.close();
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
try {
if (conn != null) {
conn.close();
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
user 表的内容是:
当我输入用户名为 "xxx" 密码为 "XXX' or '1' = '1" 时, 也可以显示登录成功, 但是根据上面显示, 数据库中显然没有这个账户和密码.
根据这一句代码
String sql = "select * from user where name = '" + un + "' and password = '" + pw + "'";
我们还原一下 sql 语句, 拼接后的 sql 语句为
select * from user where name = 'xxx' and password = 'XXX' or '1' = '1'
我们在 MySQL 中执行一下这句代码, 结果如下
由于 '1' = '1' 恒成立, MySQL 将这个表的所有信息都查找出来了, 显然这是不合理的.
在实际的工程中, 如果有别有用心之人投机取巧, 那么这个数据库中的信息是不安全的.
sql 注入的定义及其原因分析
sql 注入, 简单的讲就是, 用户的输入导致 sql 语句的含义改变, 从而导致一些不符合预期的情况出现.
我们再往深处分析, 这种现象是因为用户提供的信息和原本的 sql 语句框架进行了拼接, 之后将这个合成的 sql 语句交给 DBMS 进行编译才产生了这样的现象.
如果我们可以事先将 sql 语句的框架编译, 然后将用户的信息进行文本层面的拼接或者替换, 就不会产生这种问题了.
问题解决------ PreparedStatement
上面我们分析了问题的原因, 以及解决方法, 刚好 java 中提供了这样一个类 PreparedStatement .
Prepared 的意思是准备好的, 预编译的, 这个数据库操作对象的意思为 预先编译过的数据库操作对象
使用方法如下:
PreparedStatement ps = conn.prepareStatement(String sql);
conn 是数据库连接对象
sql 是 sql 语句框架
这个对象(PreparedStatement)的原理是: 预先对 sql 语句的框架进行编译, 然后再给 sql 语句进行传值.
下面给出例子:
String sql = "select * from user where name = ? and password = ?";
// 下面一行执行完, DBMS会将框架先编译好
PreparedStatement ps = conn.prepareStatement(sql);
其中 ? 为占位符, 代表这个位置将来可以替换
需要注意的是 PreparedStatement 的父类为 Statement , 所以它重写了父类中的 set 方法
下面讲解一下它的 set 方法: 例如
setSting(index, str) 为第 index 个 ? 获取字符串 str 填入, 会在传入的字符串两边加上单引号(第1个 ? 下标为1, 第2个 ? 下标为2, JDBC 中的下标都是从1开始的)
同理 setInt(index, int) 为获取 int 型的数据替换 ? 填入
下面我们给出改进后的代码:
import java.sql.*;
import java.util.Scanner;
// 连接数据库获取账号和密码登录
public class Test06 {
public static void main(String[] args) {
Connection conn = null;
// Statement stmt = null;
ResultSet rs = null;
PreparedStatement ps = null;
try {
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/test?characterEncoding=utf8&useSSL=true";
String username = "root";
String password = "xxxx";
conn = DriverManager.getConnection(url, username, password);
Scanner scanner = new Scanner(System.in);
System.out.print("请输入用户名: ");
String un = scanner.nextLine();
System.out.print("请输入密 码: ");
String pw = scanner.nextLine();
// 下面代码存在 sql 注入问题
// String sql = "select * from user where name = '" + un
// + "' and password = '" + pw + "'";
// stmt = conn.createStatement();
// // 下面这个代码的含义为: 将sql语句给DBMS, DBMS进行编译
// // 是先提交后编译, 所以如果用户提供了非法信息, 就导致了 sql 语句含义被扭曲
// // 从而出现不符合用户需求的情况
// rs = stmt.executeQuery(sql);
// 改进
// sql 注入是因为用户提供的信息也参与编译了
// 只要用户的信息不参与编译, 问题就解决了
// 数据库操作对象使用 PreparedStatement
// 这个对象属于预编译的数据库操作对象
// 原理是预先对 sql 语句的框架进行编译, 然后再给 sql 语句进行传值
// 下面的字符串为 sql 语句的框架 其中?为占位符
// 一个?将来接受一个值, 注意 占位符不能用单引号引起来
String sql = "select * from user where name = ? and password = ?";
// 下面一行执行完, DBMS会将框架先编译好
ps = conn.prepareStatement(sql);
// 下面给?传值, 第1个?下标为1, 第2个?下标为2, JDBC 中的下标都是从1开始的
// setSting(index, str) 为,为第index个?获取字符串str填入,会在传入的字符串两边加上单引号
// 所以不用?两边不用加单引号
// setInt(index, int) 为获取int型的数据
ps.setString(1, un);
ps.setString(2, pw);
// 执行 sql 语句, 下面的函数就不用传入 sql 语句了, 因为此时的数据库操作对象中已经有了sql的信息
rs = ps.executeQuery();
if (rs.next()) {
System.out.println("登录成功");
}else {
System.out.println("登录失败");
}
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
} finally {
try {
if (rs != null) {
rs.close();
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
try {
// if (stmt != null) {
// stmt.close();
// }
if (ps != null) {
ps.close();
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
try {
if (conn != null) {
conn.close();
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
测试一下:
注入问题解决, nice.
PreparedStatement 和 Statement 比较
1. PreparedStatement 解决了注入问题, 而 Statement 没有解决注入问题
2. PreparedStatement 编译一次可以执行多次, 而 Statement 编译一次执行一次
3. PreparedStatement 会在编译期做类型的安全检查, 而 Statement 不会做类型的安全检查, 例如 setString(1, 100) 编译期就会报错.
综上所述, 大部分情况使用 PreparedStatement , 只有极少数情况下需要使用 Statement
比如就是需要 sql 注入的时候, 就是要进行 sql 语句拼接的时候:
比如, 淘宝中的产品升序或者降序排列, 我们需要使用关键字 desc , 这里如果使用 ? 替换的方式将不会对这个关键字编译, 而且替换的是'desc', 就会出现问题
下面总结何时使用PreparedStatement 和 Statement
PreparedStatement : 单纯的进行sql语句传值, 则可以使用
Statement : 必须要sql注入, 必须进行sql语句拼接的时候使用这个对象
今天就分享到这里, 希望大家多多评论.