声明此篇文章部分图片引用了百战尚学堂的
首先先给大家看一下JDBC的编写步骤,便于后边的讲解
什么是PreparedStatement对象:
继承自 Statement 接口,由 preparedStatement方法创建。PreparedStatement具有预编译SQL语句能力,所以PreparedStatement 对象比 Statement 对象的效率更高,由于实现了动态的参数绑定,所以可以防止 SQL 注入,所以我们一般都使用 PreparedStatement。
PreparedStatement执行sql语句的编写顺序:
1、获取Connection对象
2、编写sql语句
3、通过Connection对象的prepareStatement(sql)方法传递sql语句获取PreparedStatement对象
4、sql语句中的参数绑定,需要传递?的位置(?的位置从1开始)
5、PreparedStatement对象调用execute()方法执行sql语句
如何获取Connection对象:
可以通过该链接查看Connection对象的创建:
JDBC的Statement对象的使用https://blog.csdn.net/cccccccmmm/article/details/126415156
PreparedStatement对象的使用:
为了方便使用我们先写一个JdbcUtils工具类,通过该类可以实现数据库驱动的加载,Connection对象的创建,Connection对象、PreparedStatement对象、ResultSet对象的关闭
/**
* Jdbc工具类
*/
public class JdbcUtils {
private static String url;
private static String name;
private static String pwd;
static{
try(//通过字节输入流读取properties文件
FileInputStream fis = new FileInputStream("src/jdbc.properties")){
//实例化Properties对象
Properties prop = new Properties();
//解析读取到的properties文件
prop.load(fis);
//读取连接数据库的url
url = prop.getProperty("url");
//获取用户名
name = prop.getProperty("username");
//获取密码
pwd = prop.getProperty("pwd");
//获取数据库驱动全名
String driver = prop.getProperty("driver");
//通过反射机制加载数据库驱动
Class.forName(driver);
}catch (Exception e){
e.printStackTrace();
}
}
//获取数据库连接对象
public static Connection getConnection(){
Connection connection = null;
try {
connection = DriverManager.getConnection(url,name,pwd);
} catch (SQLException e) {
e.printStackTrace();
}
return connection;
}
//关闭连接对象
public static void closeConnection(Connection connection){
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
//提交事务
public static void commit(Connection connection){
try {
connection.commit();
} catch (SQLException e) {
e.printStackTrace();
}
}
//事务回滚
public static void rollback(Connection connection){
try {
connection.rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
//关闭Statement对象
public static void closeStatement(Statement statement){
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
//关闭ResultSet
public static void closeResultSet(ResultSet resultSet){
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
//DML操作时关闭资源
public static void closeResource(Statement statement,Connection connection){
//先关闭Statement对象
closeStatement(statement);
//再关闭Connection对象
closeConnection(connection);
}
//DQL操作时关闭资源
public static void closeResource(ResultSet resultSet,Statement statement,Connection connection){
//先关闭ResultSet
closeResultSet(resultSet);
//再关闭Statement
closeStatement(statement);
//最后关闭Connection
closeConnection(connection);
}
}
我们通过PreparedStatement对象实现一下添加数据到数据库表中的方法,那添加数据首先得有表把,所以我们这里创建一个表叫users,userid为主键列,之后的代码都是对这个users表进行操作
根据我上面给的编写顺序一步步写出来,这里主要注意的是sql语句中占位符由问号(?)占位,sql语句预编译过后,sql语句需要绑定参数,而绑定参数我们通过PreparedStatement对象的set参数类型(问号的位置(从1开始),参数)
例如:setString(1,username)
/**
* 添加用户
*/
public void insertUser(String username,int userage){
Connection connection = null;
PreparedStatement ps = null;
try{
//获取数据库连接对象
connection = JdbcUtils.getConnection();
//定义sql语句,?是PreparedStatement对象中绑定参数的占位符
//?的位置是从1开始计数的
String sql = "insert into users values(default,?,?)";
//创建PreparedStatement
ps = connection.prepareStatement(sql);
//完成参数绑定
ps.setString(1,username);
ps.setInt(2,userage);
//执行sql语句
int ret = ps.executeUpdate();
System.out.println(ret);
}catch (Exception e){
e.printStackTrace();
}finally{
JdbcUtils.closeResource(ps,connection);
}
}
此时我们再写一个测试类:
运行过后就可以到数据库中查看表中是否有添加数据了
什么是SQL注入?
可以用它来从数据库获取敏感信息、利用数据库的特性执行添加用户、导出文件等一系列恶意操作,甚至有可能获取数据库乃至系统用户最高权限。
造成SQL注入的原因
程序没有有效过滤用户的输入,使攻击者成功的向服务器提交恶意的 SQL 脚本,程序在接收后错误的将攻击者的输入作为 SQL 语句的一部分执行,导致原始的查询逻辑被改变,执行了攻击者精心构造的恶意 SQL 语句。
为什么PreparedStatement对象可以防止SQL注入?
为什么说PreparedStatement对象可以避免SQL注入呢?这就是因为PreparedStatement有sql语句预编译的操作,我们刚刚上面对PreparedStatement对象进行了使用,我们会发现sql语句和需要传递的参数是分离的,这就不会导致改变我们查询代码的逻辑
我们使用Statement对象给大家看看SQL注入是怎么样的:
现在我们表中有这么些数据
我们对users表进行简单的查询操作,条件是username为我们给定的参数并且userage为我们给定的参数
我们给定的参数如果这么写,那么就改变了我们查询语句的逻辑,我们想要的是查询对应的用户信息和年龄,我们这么写直接就可以查询到所有的用户信息
我们把这个sql语句打印出来看看:
此时我们会发现我们查询了条件为username = 'oldlu' 或者 1=1的用户信息,1=1永远是正确的所以这就改变了我们的查询逻辑,直接查出了所有用户的信息,后面的--代表注释
因为Statement对象是通过字符串拼接的方式执行sql语句得,所以如果有人别有用心的话,就可以通过sql注入的方式改变我们的查询逻辑,查询出所有人的信息
但是PreparedStatement对象不会有这个问题,为什么呢?刚刚说过了,因为该对象将sql语句和传递的参数分离了,实现动态绑定参数的方式
我们再实现一个无法sql注入的方法,该方法就是使用PreparedStatement对象实现的
此时该方法执行的顺序是:
先传递sql语句:"select * from users where username = ? and userage = ?"
然后绑定参数,问号会自动识别参数类型,如果为字符串类型会自动加上单引号
此时我们就会发现我们不管怎样写都不会再有sql注入的问题,mysql会将他视为就是一个参数,不会再改变查询的逻辑
public void noSqlInject(String username,int userage){
Connection connection = null;
PreparedStatement ps = null;
ResultSet rs = null;
try{
//获取连接器对象
connection = JdbcUtils.getConnection();
//编写sql语句
String sql = "select * from users where username = ? and userage = ?";
//获取PreparedStatement对象,并且预编译sql语句
ps = connection.prepareStatement(sql);
//动态绑定sql语句
ps.setString(1,username);
ps.setInt(2,userage);
//执行sql语句
rs = ps.executeQuery();
while(rs.next()){
int userid = rs.getInt("userid");
String name = rs.getString("username");
int age = rs.getInt("userage");
System.out.println(userid +" "+ username +" "+ userage);
}
}catch(Exception e){
e.printStackTrace();
}finally{
JdbcUtils.closeResource(rs,ps,connection);
}
}
编译代码之后没有查询结果
记住PreparedStatement对象执行sql语句的编写顺序:
1、获取Connection对象
2、编写sql语句
3、通过Connection对象的prepareStatement(sql)方法传递sql语句获取PreparedStatement对象
4、sql语句中的参数绑定,需要传递?的位置(?的位置从1开始)
5、PreparedStatement对象调用execute()方法执行sql语句