1、概述
PreparedStatement
接口继承了 Statement
,并与之在两方面有所不同,它表示预编译的 SQL
语句对象。
首先,数据库会对预编译语句提供性能优化。因为预编译语句有可能被重复调用,所以语句被数据库编译后的执行代码被缓存下来,那么下次调用时只要是相同的预编译语句就不需要编译,只要将参数直接传入编译过的语句执行代码中就会得到执行。这并不是说只有一个 Connection
中多次执行的预编译语句被缓存,而是对于整个数据库,只要预编译的语句语法和缓存中匹配,在任何时候都可以不需要再次编译而直接执行。而 statement
的语句即使是相同一操作,而由于每次操作的数据不同所以使整个语句相匹配的机会极小,几乎不太可能匹配。
其次,PreparedStatement
对象中的 SQL
语句可具有一个或多个 IN 参数
。IN 参数
的值在 PreparedStatement
创建时未被指定,而是为每个IN参数
保留一个问号(“?”)作为占位符。设置IN参数
值的设置方法(setInt、setString等等)必须指定与输入参数的已定义 SQL
类型兼容的类型。如果IN参数具有 SQL
类型 INTEGER
,那么应该使用 setInt
方法。
如果需要任意参数类型转换,使用 setObject
方法时应该将目标 SQL
类型作为其参数。
设置参数:
1 PreparedStatement pstmt = con.prepareStatement("UPDATE EMPLOYEESSET SALARY = ? WHERE ID = ?"); 2 pstmt.setBigDecimal(1, 153833.00); 3 pstmt.setInt(2, 110592);
再次,PreparedStatement
可以防止 SQL
注入。由于 SQL
预先在数据库中编译成了类似“函数”的可执行程序,而为占位符赋值相当于为函数传参,这个过程中就避免了字符串拼接的问题,从而可以防止 SQL
注入。
2、核心方法
void addBatch() | 将一组SQL添加到此PreparedStatement对象的批处理命令中 |
boolean execute() | 在此PreparedStatement对象中执行SQL语句,该语句可以是任何种类的SQL语句 |
ResultSet executeQuery() | 在此PreparedStatement对象中执行SQL查询,并返回该查询生成的ResultSet对象 |
int executeUpdate() | 在此PreparedStatement对象中执行SQL语句,该语句必须是一个SQL数据操作语言(Data Manipulation Language,DML)语句,比如INSERT、UPDATE或DELETE语句;或者是无返回内容的SQL语句,比如DDL语句 |
void setDate(int parameterIndex, Date x) | 使用默认时区将指定参数设置为给定java.sql.Date值。parameterIndex - 第一个参数是1,第二个参数是2,…… |
void setDouble(int parameterIndex, double x) | 将指定参数设置为给定Java double值 |
void setInt(int parameterIndex, int x) | 将指定参数设置为给定Java int值 |
void setLong(int parameterIndex, long x) | 将指定参数设置为给定Java long值 |
void setObject(int parameterIndex, Object x) | 使用给定对象设置指定参数的值 |
void setString(int parameterIndex, String x) | 将指定参数设置为给定Java String值 |
void setTimestamp(int parameterIndex, Timestamp x) | 将指定参数设置为给定java.sql.Timestamp值 |
3、SQL 注入
就是攻击者恶意的利用字符串拼接和 SQL
逻辑运算的特点对数据库数据、服务器配置的试探性的攻击。
这种攻击使用网站页面的输入框,或者使用程序发起 http 请求即可实现,这种方式不能被服务器的防火墙拦截,在分析服务器日志的时候才有可能发现,需要进行客户端 IP 限制才有可能在一定程度上防止。
下面看几个简单的 SQL
注入攻击的例子。
首先,为 UserDao
类加一个方法 getUsersInfoByName
根据用户名模糊查询用户信息
1 public List<Map<String, Object>> getUsersInfoByName(String name) throws SQLException { 2 3 // 拼接sql字符串 4 String sql = "select id, username, role_id from t_user where username like '%" + name + "%'"; 5 6 System.out.println(sql); // 打印一下SQL 7 8 Connection conn = null; 9 Statement stmt = null; 10 ResultSet rs = null; 11 12 try { 13 // 获取连接 14 conn = DBUtil.getConnection(); 15 16 stmt = conn.createStatement(); 17 // 执行查询并获取结果集 18 rs = stmt.executeQuery(sql); 19 20 List<Map<String, Object>> users = new ArrayList<Map<String, Object>>(); 21 22 // 遍历结果集,封装数据并返回 23 while (rs.next()) { 24 Map<String, Object> user = new HashMap<String, Object>(); 25 user.put("id", rs.getInt(1)); 26 user.put("username", rs.getString("username")); 27 user.put("role_id", rs.getInt(3)); 28 29 users.add(user); 30 } 31 return users; 32 } catch (SQLException e) { 33 // 可以把异常抛给业务层的调用者 34 throw e; 35 } finally { 36 // 关闭连接,释放资源 37 // 略 38 } 39 }
在演示之前,我们再分析一下上面这段代码的运行环境。
1)页面上的用户信息展示,可以使用用户名进行搜索
2)使用者输入用户名后进行查询,WEB 服务器找到控制层获取到参数用户名,再调用业务层,业务层再调用上面的这个 DAO 方法进行数据查询
3)查询到的数据层层返回,最后返回给浏览器进行展示
为了方便,我们使用 main 方法模仿业务层调用 DAO 方法
示例1:获取登陆用户
场景就是攻击者输入!@#$%^%' or user() like '%root
拼接成的 SQL 字符串就是这样的:
select id, username, role_id from t_user where username like '%!@#$%^%' or user() like '%root%'
而返回的数据是全部用户信息,这样攻击者就可以确定服务器使用的是 MySQL 数据库,而且是 root 用户连接
示例2:获取库
场景就是攻击者输入!@#$%^%' or database() like '%test
拼接成的 SQL 字符串就是这样的:
select id, username, role_id from t_user where username like '%!@#$%^%' or database() like '%test%'
攻击者就可以确定服务器使用的是 test 库
示例3:获取 MySQL 版本
场景就是攻击者输入!@#$%^%' or version() like '%5.5
拼接成的 SQL 字符串就是这样的:
select id, username, role_id from t_user where username like '%!@#$%^%' or version() like '%5.5%'
攻击者就可以确定数据库版本是5.5
我们的例子比较简单,真实的场景更加复杂、曲折,后果也更加惊心动魄
4、优化的 UserDao
1 public Map<String, Object> getUserInfoById(int id) throws SQLException { 2 3 // sql字符串 4 String sql = "select id, username, role_id from t_user where id = ?"; 5 6 Connection conn = null; 7 PreparedStatement prep = null; 8 ResultSet rs = null; 9 10 try { 11 // 获取连接 12 conn = DBUtil.getConnection(); 13 14 // 获取PreparedStatement 15 prep = conn.prepareStatement(sql); 16 // 设置参数 17 prep.setInt(1, id); 18 19 // 执行查询并获取结果集 20 rs = prep.executeQuery(); 21 22 Map<String, Object> user = new HashMap<String, Object>(); 23 24 // 遍历结果集,封装数据并返回 25 if (rs.next()) { 26 user.put("id", id); 27 user.put("username", rs.getString("username")); 28 user.put("role_id", rs.getInt(3)); 29 } 30 return user; 31 } catch (SQLException e) { 32 // 可以把异常抛给业务层的调用者 33 throw e; 34 } finally { 35 // 关闭连接,释放资源 36 // 略 37 } 38 } 39 40 public List<Map<String, Object>> getUsersInfoByName(String name) throws SQLException { 41 42 // sql字符串 43 String sql = "select id, username, role_id from t_user where username like ?"; 44 45 Connection conn = null; 46 PreparedStatement prep = null; 47 ResultSet rs = null; 48 49 try { 50 // 获取连接 51 conn = DBUtil.getConnection(); 52 53 // 获取PreparedStatement 54 prep = conn.prepareStatement(sql); 55 56 // 设置参数 57 prep.setString(1, "%" + name + "%"); 58 59 // 执行查询并获取结果集 60 rs = prep.executeQuery(); 61 62 List<Map<String, Object>> users = new ArrayList<Map<String, Object>>(); 63 64 // 遍历结果集,封装数据并返回 65 while (rs.next()) { 66 Map<String, Object> user = new HashMap<String, Object>(); 67 user.put("id", rs.getInt(1)); 68 user.put("username", rs.getString("username")); 69 user.put("role_id", rs.getInt(3)); 70 71 users.add(user); 72 } 73 return users; 74 } catch (SQLException e) { 75 // 可以把异常抛给业务层的调用者 76 throw e; 77 } finally { 78 // 关闭连接,释放资源 79 // 略 80 } 81 }
5、批处理
1 String sql = "insert into t_user (username) values (?)"; 2 3 Connection conn = null; 4 PreparedStatement prep = null; 5 ResultSet rs = null; 6 7 try { 8 // 获取连接 9 conn = DBUtil.getConnection(); 10 // 设置手动提交 11 conn.setAutoCommit(false); 12 // 获取PreparedStatement 13 prep = conn.prepareStatement(sql); 14 15 for (int i = 1; i <= 1000000; i++) { 16 prep.setString(1, String.format("%s%05d", "admin", i)); 17 prep.addBatch(); 18 } 19 // 执行批量插入操作 20 prep.executeBatch(); 21 // 提交事务 22 conn.commit(); 23 24 } catch (SQLException e) { 25 // 可以把异常抛给业务层的调用者 26 throw e; 27 } finally { 28 // 关闭连接,释放资源 29 // 略 30 }