- SQL注入
- 预编译语句对象
- CURD操作
- 批处理
- 事务及回滚点
SQL注入
介绍
SQL注入攻击指的是通过构建特殊的输入作为参数传入Web应用程序,而这些输入大都是SQL语法里的一些组合,通过执行SQL语句进而执行攻击者所要的操作,其主要原因是程序没有细致地过滤用户输入的数据,致使非法数据侵入系统。
案例
以模拟登录为例:在前台输入用户名和密码,后台判断信息是否正确,并给出前台反馈信息,前台输出反馈信息。
创建实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| package com.excepenxi.entity;
import java.io.Serializable;
public class Account implements Serializable { private int aid; private String username; private String password; private int money;
@Override public String toString() { return "Account{" + "aid=" + aid + ", username='" + username + '\'' + ", password='" + password + '\'' + ", money=" + money + '}'; }
public Account(int aid, String username, String password, int money) { this.aid = aid; this.username = username; this.password = password; this.money = money; }
public Account() { }
public int getAid() { return aid; }
public void setAid(int aid) { this.aid = aid; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public int getMoney() { return money; }
public void setMoney(int money) { this.money = money; } }
|
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| package com.excepenxi.test2;
import com.excepenxi.entity.Account; import java.sql.*; import java.util.Scanner;
public class TestInjection { private static String driver ="com.mysql.cj.jdbc.Driver"; private static String url="jdbc:mysql://127.0.0.1:3306/mydb?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"; private static String user="root"; private static String password="root";
public static void main(String[] args) {
Scanner sc =new Scanner(System.in); System.out.print("请输入用户名:"); String username=sc.next(); System.out.print("请输入密码:"); String pwd =sc.next(); Account account = getAccount(username, pwd); System.out.println(null != account?"登录成功":"登录失败"); sc.close(); } public static Account getAccount(String username,String pwd){ Connection connection = null; Statement statement=null; ResultSet resultSet=null; Account account =null;
try{ Class.forName(driver); connection = DriverManager.getConnection(url, user,password); statement = connection.createStatement(); String sql="select * from account where username ='"+username+"' and password ='"+pwd+"'"; System.out.println(sql); resultSet = statement.executeQuery(sql); while(resultSet.next()){ int aid = resultSet.getInt("aid"); String usernamea = resultSet.getString("username"); String pwda = resultSet.getString("password"); int money = resultSet.getInt("money"); account = new Account(aid,usernamea,pwda,money); System.out.println(account); } }catch (Exception e){ e.printStackTrace(); }finally { if(null != resultSet){ try { resultSet.close(); } catch (SQLException e) { e.printStackTrace(); } } if(null != statement){ try { statement.close(); } catch (SQLException e) { e.printStackTrace(); } } if(null != connection){ try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } return account; } }
|
测试结果
当输入了精心设计的用户名密码后,即使是错误的,也能登录成功。让登录功能形同虚设。这是为什么呢,这就是SQL注入风险,原因在于SQL语句是字符串拼接的。SQL语句中拼接的内容破坏了SQL语句原有的判断逻辑
防止SQL注入
如何解决呢?使用PreparedStatement预编译语句对象
就可以解决掉。
预编译语句对象
使用预编译语句对象防止注入攻击
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| package com.excepenxi.test2;
import com.excepenxi.entity.Account;
import java.sql.*; import java.util.Scanner;
public class TestInjection2 { private static String driver ="com.mysql.cj.jdbc.Driver"; private static String url="jdbc:mysql://127.0.0.1:3306/mydb?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"; private static String user="root"; private static String password="root";
public static void main(String[] args) {
Scanner sc =new Scanner(System.in); System.out.print("请输入用户名:"); String username=sc.next(); System.out.print("请输入密码:"); String pwd =sc.next(); Account account = getAccount(username, pwd); System.out.println(null != account?"登录成功":"登录失败"); sc.close(); } public static Account getAccount(String username,String pwd){ Connection connection = null; PreparedStatement preparedStatement=null; ResultSet resultSet=null; Account account =null;
try{ Class.forName(driver); connection = DriverManager.getConnection(url, user,password);
String sql="select * from account where username = ? and password = ?"; preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1,username ); preparedStatement.setString(2,pwd ); resultSet = preparedStatement.executeQuery(); while(resultSet.next()){ int aid = resultSet.getInt("aid"); String usernamea = resultSet.getString("username"); String pwda = resultSet.getString("password"); int money = resultSet.getInt("money"); account = new Account(aid,usernamea,pwda,money); System.out.println(account); } }catch (Exception e){ e.printStackTrace(); }finally { if(null != resultSet){ try { resultSet.close(); } catch (SQLException e) { e.printStackTrace(); } } if(null != preparedStatement){ try { preparedStatement.close(); } catch (SQLException e) { e.printStackTrace(); } } if(null != connection){ try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } return account; } }
|
测试结果
prepareStatment对象在set***方法上,会对单引号进行转译处理,也就是说,?中的数据的单引号 ‘ 会被转义成 \’,这样就单引号就不会破坏sql语句的结构。
1 2 3 4 5 6 7 8 9
| SELECT * FROM users WHERE userName = ? AND password = ?
preparedStatement.setString(1,"xiaoming"); preparedStatement.setString(2,'anything' OR 'x'='x'); # 会被转义为 SELECT * FROM users WHERE userName = 'xiaoming' AND password = 'anything\' OR\'x\'=\'x\'' # 而不是 SELECT * FROM users WHERE userName = 'xiaoming' AND password = 'anything' OR 'x'='x'
|
说白了就是把值当中的所有单引号给转义了! 这就达到了防止sql注入的目的,说白了mysql驱动的PreparedStatement实现类的setString();方法内部做了单引号的转义,而Statement不能防止sql注入,就是因为它没有把单引号做转义,而是简单粗暴的直接拼接字符串,所以达不到防止sql注入的目的。
预编译(mysql优化)
当客户端发送一条sql语句给DBMS时,MySQL的执行流程如下:
客户端向服务器端发送SQL命令
服务器端连接模块连接并验证
缓存模块解析SQL为Hash并与缓存中Hash表对应。如果有结果直接返回结果,如果没有对应继续向下执行
解析器解析SQL为解析树,如果出现错误,报SQL解析错误。如果正确,向下传递
预处理器对解析树继续处理,处理成新的解析树。
优化器根据开销自动选择最优执行计划,生成执行计划
执行器执行执行计划,访问存储引擎接口
存储引擎访问物理文件并返回结果
如果开启缓存,缓存管理器把结果放入到查询缓存中。
返回结果给客户端
当客户发送一条SQL语句给DBMS后,DBMS总是需要校验SQL语句的语法格式是否正确,然后把SQL语句编译成可执行的函数,最后才是执行SQL语句。其中校验语法,和编译所花的时间可能比执行SQL语句花的时间还要多。
预编译语句PreparedStatement 是java.sql中的一个接口,它是Statement的子接口。通过Statement对象执行SQL语句时,需要将SQL语句发送给DBMS,由DBMS首先进行编译后再执行。预编译语句和Statement不同,在创建PreparedStatement 对象时就指定了SQL语句,该语句立即发送给DBMS进行编译。当该编译语句被执行时,DBMS直接运行编译后的SQL语句,而不需要像其他SQL语句那样首先将其编译。预编译的SQL语句处理性能稍微
高于普通的传递变量的办法。
例如:我们需要执行多次insert语句,但只是每次插入的值不同,MySQL服务器也是需要每次都去校验SQL语句的语法格式,以及编译,这就浪费了太多的时间。如果使用预编译功能,那么只对SQL语句进行一次语法校验和编译,所以效率要高。
开启预编译
我们可以通过设置URL中的参数来控制预编译是否开启
1 2 3
| "jdbc:mysql://localhost:3306/mydb *****&useServerPrepStmts=true&cachePrepStmts=true"; # useServerPrepStmts=true 开启预编译 # cachePrepStmts=true 开启预编译缓存,提升性能;如果长期查询一次,开启反而占用缓存
|
值得注意的是,我们的Connector/J 5.0.5及之后useServerPrepStmts默认false,就是默认没有开启预编译,之前默认为true, cachePrepStmts 一直默认为false,需要我们手动设置才可以启用预编译,在开启预编译的同时要同时开启预编译缓存才能带来些许的性能提升
Statement和PreparedStatment的关系和区别
关系:public interface PreparedStatement extends Statement
区别:
PreparedStatment安全性高,可以避免SQL注入
PreparedStatment简单不繁琐,不用进行字符串拼接
PreparedStatment性能高,用在执行多个相同数据库DML操作时,可以减少sql语句的编译次数
PreparedStatment完成CURD操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
| package com.excepenxi.test3;
import com.excepenxi.entity.Emp;
import java.sql.*; import java.util.ArrayList; import java.util.List;
public class TestPreparedSstatement { private static String driver ="com.mysql.cj.jdbc.Driver"; private static String url="jdbc:mysql://127.0.0.1:3306/mydb?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"; private static String user="root"; private static String password="root";
public static void main(String[] args) { testQuery(); }
public static void testAdd(){ Connection connection = null; PreparedStatement preparedStatement=null; try{ Class.forName(driver); connection = DriverManager.getConnection(url, user,password); String sql="insert into emp values(DEFAULT,?,?,?,?,?,?,?)"; preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1,"Mark"); preparedStatement.setString(2,"MANAGER" ); preparedStatement.setInt(3,7839); preparedStatement.setDate(4,new Date(System.currentTimeMillis())); preparedStatement.setDouble(5,3000.12); preparedStatement.setDouble(6,0.0); preparedStatement.setDouble(7,30); int rows =preparedStatement.executeUpdate(); System.out.println(rows); }catch (Exception e){ e.printStackTrace(); }finally { if(null != preparedStatement){ try { preparedStatement.close(); } catch (SQLException e) { e.printStackTrace(); } } if(null != connection){ try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } } public static void testUpdate(){ Connection connection = null; PreparedStatement preparedStatement=null; try{ Class.forName(driver); connection = DriverManager.getConnection(url, user,password); String sql="update emp set ename =? ,job=? where empno =?"; preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1,"Jhon"); preparedStatement.setString(2,"ANALYST" ); preparedStatement.setInt(3,10001); int rows =preparedStatement.executeUpdate(); System.out.println(rows); }catch (Exception e){ e.printStackTrace(); }finally { if(null != preparedStatement){ try { preparedStatement.close(); } catch (SQLException e) { e.printStackTrace(); } } if(null != connection){ try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } } public static void testDelete(){ Connection connection = null; PreparedStatement preparedStatement=null; try{ Class.forName(driver); connection = DriverManager.getConnection(url, user,password); String sql="delete from emp where empno =?"; preparedStatement = connection.prepareStatement(sql); preparedStatement.setInt(1,10001); int rows =preparedStatement.executeUpdate(); System.out.println(rows); }catch (Exception e){ e.printStackTrace(); }finally { if(null != preparedStatement){ try { preparedStatement.close(); } catch (SQLException e) { e.printStackTrace(); } } if(null != connection){ try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } } public static void testQuery(){ Connection connection = null; PreparedStatement preparedStatement=null; ResultSet resultSet=null; List<Emp> list =null; try{ Class.forName(driver); connection = DriverManager.getConnection(url, user,password);
String sql="select * from emp where ename like ? "; preparedStatement = connection.prepareStatement(sql); preparedStatement.setString(1,"%A%"); resultSet = preparedStatement.executeQuery(); list=new ArrayList<Emp>() ; while(resultSet.next()){ int empno = resultSet.getInt("empno"); String ename = resultSet.getString("ename"); String job = resultSet.getString("job"); int mgr = resultSet.getInt("mgr"); Date hiredate = resultSet.getDate("hiredate"); double sal= resultSet.getDouble("sal"); double comm= resultSet.getDouble("comm"); int deptno= resultSet.getInt("deptno"); Emp emp =new Emp(empno, ename, job, mgr, hiredate, sal, comm, deptno); list.add(emp); } }catch (Exception e){ e.printStackTrace(); }finally { if(null != resultSet){ try { resultSet.close(); } catch (SQLException e) { e.printStackTrace(); } } if(null != preparedStatement){ try { preparedStatement.close(); } catch (SQLException e) { e.printStackTrace(); } } if(null != connection){ try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } for (Emp emp : list) { System.out.println(emp); } } }
|
批处理
当我们有多条sql语句需要发送到数据库执行的时候,有两种发送方式,一种是执行一条发送一条sql语句给数据库,另一个种是发送一个sql集合给数据库,也就是发送一个批sql到数据库。普通的执行过程是:每处理一条数据,就访问一次数据库;而批处理是:累积到一定数量,再一次性提交到数据库,减少了与数据库的交互次数,所以效率会大大提高,很显然两者的数据库执行效率是不同的,我们发送批处理sql的时候数据库执行效率要高。
statement语句对象实现批处理有如下问题
- 缺点:采用硬编码效率低,安全性较差。
- 原理:硬编码,每次执行时相似SQL都会进行编译
PreparedStatement+批处理
- 优点:语句只编译一次,减少编译次数。提高了安全性(阻止了SQL注入)
- 原理:相似SQL只编译一次,减少编译次数
- 注意: 需要设置批处理开启&rewriteBatchedStatements=true
批处理之增加数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| package com.excepenxi.test4;
import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException;
public class TestBatch { private static String driver ="com.mysql.cj.jdbc.Driver";
private static String url="jdbc:mysql://127.0.0.1:3306/mydb?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useServerPrepStmts=true&cachePrepStmts=true&rewriteBatchedStatements=true"; private static String user="root"; private static String password="root";
public static void main(String[] args) { testAddBatch(); } public static void testAddBatch(){ Connection connection = null; PreparedStatement preparedStatement=null; try{ Class.forName(driver); connection = DriverManager.getConnection(url, user,password); String sql="insert into dept values (DEFAULT ,?,?)"; preparedStatement = connection.prepareStatement(sql); for (int i = 1; i <= 10663; i++) { preparedStatement.setString(1, "name"); preparedStatement.setString(2, "loc"); preparedStatement.addBatch(); if(i%1000==0){ preparedStatement.executeBatch(); preparedStatement.clearBatch(); } }
preparedStatement.executeBatch(); preparedStatement.clearBatch(); }catch (Exception e){ e.printStackTrace(); }finally { if(null != preparedStatement){ try { preparedStatement.close(); } catch (SQLException e) { e.printStackTrace(); } } if(null != connection){ try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }
|
1 2 3 4 5
| SELECT * FROM dept WHERE deptno > 50;
SELECT COUNT(*) FROM dept WHERE deptno > 50;
DELETE FROM dept WHERE deptno > 50;
|
补充
- 增加批量写的速度: useServerPrepStmts=false&rewriteBatchedStatements=true&useCompression=true
- 增加读的速度: useServerPrepStmts=true&cachePrepStmts=true
批处理之修改数据
待补充……
批处理之删除数据
待补充……
事务及回滚点
JDBC中使用事务
事务概念:在逻辑上一组不可分割的操作,由多个sql语句组成,多个sql语句要么全都执行成功,要么都不执行. 原子性 一致性 隔离性 持久性
JDBC控制事物主要就是在学习如何让多个数据库操作成为一个整体,实现要么全都执行成功,要么全都不执行
在JDBC中,事务操作是自动提交。一条对数据库的DML(insert、update、delete)代表一项事务操作,操作成功后,系统将自动调用commit()提交,否则自动调用rollback()回滚,在JDBC中,事务操作方法都位于接口java.sql.Connection中,可以通过调用setAutoCommit(false)来禁止自动提交。之后就可以把多个数据库操作的表达式作为一个事务,在操作完成后调用commit()来进行整体提交,倘若其中一个表达式操作失败,都不会执行到commit(),并且将产生响应的异常;此时就可以在异常捕获时调用rollback()进行回滚,回复至数据初始状态.事务开始的边界则不是那么明显了,它会开始于组成当前事务的所有statement中的第一个被执行的时候。事务结束的边界是commit或者rollback方法的调用
使用事务保证转账安全性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| package com.excepenxi.test5;
import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException;
public class TestTransaction { private static String driver ="com.mysql.cj.jdbc.Driver"; private static String url="jdbc:mysql://127.0.0.1:3306/mydb?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useServerPrepStmts=true&cachePrepStmts=true&rewriteBatchedStatements=true"; private static String user="root"; private static String password="root"; public static void main(String[] args) { testTransaction(); } public static void testTransaction(){ Connection connection = null; PreparedStatement preparedStatement=null;
try{ Class.forName(driver); connection = DriverManager.getConnection(url, user,password); connection.setAutoCommit(false); String sql="update account set money =money- ? where aid = ?"; preparedStatement = connection.prepareStatement(sql); preparedStatement.setDouble(1, 100); preparedStatement.setInt(2, 1); preparedStatement.executeUpdate(); preparedStatement.setDouble(1, -100); preparedStatement.setInt(2, 2); preparedStatement.executeUpdate(); }catch (Exception e){ if(null != connection){ try { connection.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); }finally { if(null != connection){ try { connection.commit(); } catch (SQLException e) { e.printStackTrace(); } } if(null != preparedStatement){ try { preparedStatement.close(); } catch (SQLException e) { e.printStackTrace(); } } if(null != connection){ try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }
|
设置回滚点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| package com.excepenxi.test5;
import java.sql.*; import java.util.LinkedList;
public class TestTransaction2 { private static String driver ="com.mysql.cj.jdbc.Driver"; private static String url="jdbc:mysql://127.0.0.1:3306/mydb?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useServerPrepStmts=true&cachePrepStmts=true&&rewriteBatchedStatements=true"; private static String user="root"; private static String password="root"; public static void main(String[] args) { testAddBatch(); } public static void testAddBatch(){ Connection connection = null; PreparedStatement preparedStatement=null; LinkedList<Savepoint> savepoints =new LinkedList<Savepoint>(); try{ Class.forName(driver); connection = DriverManager.getConnection(url, user,password); connection.setAutoCommit(false); String sql="insert into dept values (DEFAULT ,?,?)"; preparedStatement = connection.prepareStatement(sql); for (int i = 1; i <= 10663; i++) { preparedStatement.setString(1, "name"); preparedStatement.setString(2, "loc"); preparedStatement.addBatch(); if(i%1000==0){ preparedStatement.executeBatch(); preparedStatement.clearBatch(); Savepoint savepoint = connection.setSavepoint(); savepoints.addLast(savepoint); } if(i ==10001){ int x =1/0; } }
preparedStatement.executeBatch(); preparedStatement.clearBatch(); }catch (Exception e){ if(null != connection){ try { Savepoint sp = savepoints.get(4); if(null != sp){ connection.rollback(sp); } } catch (SQLException e2) { e2.printStackTrace(); } } e.printStackTrace(); }finally { if(null != connection){ try { connection.commit(); } catch (SQLException e) { e.printStackTrace(); } } if(null != preparedStatement){ try { preparedStatement.close(); } catch (SQLException e) { e.printStackTrace(); } } if(null != connection){ try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }
|