浅谈sql注入问题
在学习java的jdbc时学到这么一个东西,我们有时候会忽略这个问题,我们摘出来做一下简单的讨论。
Sql注入问题 及statement和PreparedStatement的区别:
首先我们来复习一下jdbc操作数据库的几个步骤:
注册驱动、获取连接、获取数据库操作对象,执行sql语句、处理查询结果集、释放资源
那么之前我们一直用的是prestatement来获取数据库操作对象,那么大家知道prestatement其实是属于预编译的数据库操作对象。那么prestatement是不是全能的呢?并不是,比如京东上的商品价格展示,就要求升序或者降序排列,那么必须拼接asc和desc时,prestatement是否依然有效呢?
我们简单模拟一个升序降序排列功能
示例一:
package jdbc;
import java.sql.*;
import java.util.Scanner;
public class JdbcTest07sql1 {
public static void main(String[] args) {
//用户控制台输入desc就是降序,输入asc就是升序
Scanner s = new Scanner(System.in);
System.out.println("请输入desc或asc,desc表示降序,asc表示升序");
System.out.println("请输入:");
String keyWords = s.nextLine();
//执行SQL
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
//注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//获取连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/orcl","root","123456");
//获取数据库操作对象
String sql = "select ename from emp order by empno ? ";
ps = conn.prepareStatement(sql);
ps.setString(1,keyWords);
//执行sql
rs=ps.executeQuery();
//遍历结果集
while (rs.next()){
System.out.println(rs.getString("ename"));
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException throwables) {
throwables.printStackTrace();
}finally {
if(rs!=null){
try {
rs.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}if(ps!=null){
try {
ps.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if (conn!=null){
try {
conn.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}
}
此程序会报这个错误:
java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ‘‘desc’’ at line 1
可以看到是由于sql语句给占位符传值时多加了一个引号。
那么此时必须用到statement,来拼接字符串,此问题就得到了解决。只展示和前面代码不同的地方
try {
//注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//获取连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/orcl","root","123456");
//获取数据库操作对象
stmt = conn.createStatement();
//执行SQL
String sql = "select ename from emp order by empno "+keyWords;
rs = stmt.executeQuery(sql);
//遍历结果集
while (rs.next()){
System.out.println(rs.getString("ename"));
}
那么使用statement就一定好吗?并不是,在进行用户登录注册时又会出现怎样的问题呢?
在这里我们来模拟一个用户登陆注册功能
package jdbc;
import java.sql.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
public class JdbcTest06sql1 {
public static void main(String[] args) {
//初始化一个界面
Map<String,String> userLoginInfo = initUI();
//验证用户名和密码
boolean loginSuccess = login(userLoginInfo);
//最后输出结果
System.out.println(loginSuccess ? "登陆成功":"登陆失败");
}
/*
初始化用户界面
@return 用户输入的用户名和密码等信息
*/
private static Map<String, String> initUI() {
Scanner s = new Scanner(System.in);
System.out.println("用户名:");
String loginName = s.nextLine();
System.out.println("密码:");
String loginPwd = s.nextLine();
Map<String,String> userLoginInfo = new HashMap<>();
userLoginInfo.put("loginName",loginName);
userLoginInfo.put("loginPwd",loginPwd);
return userLoginInfo;
}
/*
用户登录
@param userLoginInfo 用户登录信息
@return false失败 true成功
*/
private static boolean login(Map<String, String> userLoginInfo) {
//打标记
boolean loginSuccess = false;
//单独定义变量
String loginName = userLoginInfo.get("loginName");
String loginPwd = userLoginInfo.get("loginPwd");
//JBDC代码
Connection conn = null;
Statement stmt = null;//这里使用PreparedStatement预编译的数据库操作对象
ResultSet rs = null;
try {
//1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/orcl","root","123456");
//3.获取的数据库操作对象
stmt = conn.createStatement();
//4.执行sql
String sql = "select * from t_user where loginName = '"+loginName+"' and loginPwd ='"+loginPwd+"'";
rs = stmt.executeQuery(sql);
//5.处理结果集
if(rs.next()){
loginSuccess = true;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//6.释放资源
if (rs != null){
try {
rs.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if (stmt != null){
try {
stmt.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
if (conn != null){
try {
conn.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
return loginSuccess;
}
}
比如我们这样来访问数据库中的数据
用户名:zhangsan
密码:zhangsan’ or ‘1’=’1
密码不正确但依然访问到了数据库的内容
那么这种现象就被称作是sql注入现象。
这种现象被称为sql注入(安全隐患)(黑客经常使用)
sql注入的根本原因:用户输入的信息中含有sql语句的关键字,并且这些关键字参与sql语句的编译过程,导致sql语句的原意被扭曲,进而达到sql注入。
执行到rs = stmt.executeQuery(sql);时,会把sql语句发送给DBMS(数据库管理系统),会把sql语句进行编译,正好将用户提供的非法信息编译进去,导致了原sql语句含义被扭曲了
我们来看一下sql注入的定义:打开ppt
那么我们怎么解决sql注入问题呢?同样也只展示和前面代码不同的部分
//1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/orcl","root","123456");
//3.获取预编译的数据库操作对象
//SQL语句的框子。其中一个?,表示一个占位符,一个?将来接收一个”值“,注意:占位符不能使用单引号括起来。
String sql = "select * from t_user where loginName = ? and loginPwd = ?";//?称为占位符
//程序执行到此处,会发送sql语句框子给DBMS,然后DBMS进行sql语句的预先编译。
ps = conn.prepareStatement(sql);
//给占位符?传值(第一个问号下标为1,第二个问号下标为2,JDBC中所有下标从1开始。)
ps.setString(1,loginName);
ps.setString(2,loginPwd);
//4.执行sql
rs = ps.executeQuery();
//5.处理结果集
if(rs.next()){
loginSuccess = true;
}
只要用户提供的信息不参与SQL语句的编译过程,问题就解决了。即使用户提供的信息中含有SQL语句的关键字,但是没有参与编译,不起作用。要想用户信息不参与SQL语句的编译,那么必须使用java.sql.PreparedStatement。现在来看一下PreparedStatement是什么?
PreparedStatement继承了java.sql.Statement,PreparedStatement属于预编译的数据库操作对象,PreparedStatement的原理是:预先对sql语句的框架进行编译,然后再给sql语句传"值";
写到这里,我们顺便来对比一下statement及prestatement的区别:
1.Statement存在sql注入问题,PreparedStatement解决了sql注入问题
2.Statement编译一次执行一次,PreparedStatement编译一次,可执行N次。PreparedStatement效率较高一些
3.PreparedStatement会在编译阶段做类型的安全检查。
综上所述PreparedStatement使用较多,只有极少数情况下需要使用Statement。