1.JDBC中sql注入
1.1 JDBC
JDBC的概念:JDBC是sun公司(已被Oracle收购)制定一系列接口标准,由不同数据库厂商(Oracle、MySQL等)实现接口方法并封装成驱动文件,供开发人员操作数据库。也就是说,开发人员可采用统一的代码来操作不同的数据库,而无需关注驱动文件的内部实现。
通俗的理解就是JDBC是规范、是接口,不同的数据库厂商要据此编写各自的操作实现。
1.2 SQL注入
我们知道,dao层(负责操作数据库,数据访问层)中执行的SQL语句是拼接出来的,其中有一部分数据是由用户从客户端传入,所以当用户传入的数据中包含SQL关键字时,就有可能通过这些关键字改变SQL语句的语义,从而执行一些特殊的操作,这样的攻击方式就叫做SQL注入攻击。
sql注入案例演示:
private static boolean Login(Map<String, String> userLoginInfo) {
// TODO Auto-generated method stub
boolean loginSuccess= false;
String nameString= userLoginInfo.get("usernameString");
String pwdString = userLoginInfo.get("passwordString");
Connection con = null;
Statement statement = null;
ResultSet resultSet = null;
try {
//1.注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2.获取连接
con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root","");
//3.获取数据库操作对象
statement = con.createStatement();
//4.执行sql语句
String sql = "select * from user where username='"+nameString+"' and password='"+pwdString+"'";
//"select * from user where username='"+nameString+"' and password='fdas' or '1' = '1'"
System.out.println("用户名"+nameString);
System.out.println("密码"+pwdString);
resultSet = statement.executeQuery(sql);
//5.处理结果集
if (resultSet.next()) {
//登录成功
loginSuccess = true;
}
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}finally {
if (resultSet !=null) {
try {
resultSet.close();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
if (statement !=null) {
try {
statement.close();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
if (con !=null) {
try {
con.close();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
return loginSuccess;
}
程序的逻辑为:
用户输入用户名和密码
在数据库中查找匹配的用户名和密码,若有匹配的则登录成功,否则,提示"登陆失败"。实际就是在数据库中执行以下sql语句
String sql = “select * from user where username='”+nameString+“’ and password='”+pwdString+“'”;
上边代码的逻辑看似没有问题,实则问题很大:
当用户这样输入:一个数据库中不存在的用户名,例如bbb,然后输入密码:a’ or ‘a’ = 'a,这样输入竟然也能登陆成功。
如图:
原因:
当用户输入以上用户名和密码时,程序就会在数据库中执行以下sql语句
sql :select * from user where username = ‘bbb’ and password = ‘a’ or ‘a’ = ‘a’ ;
这是一条恒等式,数据库会返回user表中所有的内容,这样rs.next()就会等于true,所以可以登陆成功。
1.3 SQL注入的预防
使用PreparedStatement代替Statement可以有效防止SQL注入的发生。
PreparedStatement利用预编译的机制将sql语句的主干和参数分别传输给数据库服务器,从而使数据库分辨的出哪些是sql语句的主干哪些是参数,这样一来即使参数中带了sql的关键字,数据库服务器也仅仅将他当作参数值使用,关键字不会起作用,从而从原理上防止了sql注入的问题。
上面的sql语句改写为:
String sql = "select * from user where username= ? and password= ?";
再去给每个 ?占位符赋值
sta.setString(1, nameString);
sta.setString(2, pwdString);
使用PrepareStatement预防sql注入演示:
private static boolean Login(Map<String, String> userLoginInfo) {
// TODO Auto-generated method stub
boolean loginSuccess= false;
String nameString= userLoginInfo.get("usernameString");
String pwdString = userLoginInfo.get("passwordString");
Connection con = null;
PreparedStatement sta = null;
ResultSet resultSet = null;
try {
//1.注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2.获取连接
con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root","");
//3.获取数据库操作对象
//SQL语句的框子 其中一个?,表示一个占位符 ,一个?将来接收一个"值",注意占位符不能使用单引号括起来
String sql = "select * from user where username= ? and password= ?";//sql语句的框子
//程序执行到此处,会发送sql语句框子给DBMS,然后DBMS对sql语句进行预编译
sta = con.prepareStatement(sql);
//给占位符?传值(第一个问号的下标是1,第二个问号的下标是2,JDBC中所有下标都是从1开始的)
sta.setString(1, nameString);
sta.setString(2, pwdString);
//4.执行sql
System.out.println("用户名:"+nameString);
System.out.println("密码:"+pwdString);
resultSet = sta.executeQuery();
//5.处理结果集
if (resultSet.next()) {
//登录成功
loginSuccess = true;
}
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}finally {
if (resultSet !=null) {
try {
resultSet.close();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
if (sta !=null) {
try {
sta.close();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
if (con !=null) {
try {
con.close();
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
}
return loginSuccess;
}
测试:
2. 总结
PreparedStatement主要有如下的三个优点:
- 可以防止sql注入
- 由于使用了预编译机制,执行的效率要高于Statement
- sql语句使用?形式替代参数,然后再用方法设置?的值,比起拼接字符串,代码更加优雅.
PreparedStatement 与Statment比较:
- 语法不同:PreparedStatement可以使用预编译的sql,而Statment只能使用静态的sql
- 效率不同: PreparedStatement可以使用sql缓存区,效率比Statment高
- 安全性不同: PreparedStatement可以有效防止sql注入,而Statment不能防止sql注入。
搭建数据库表的代码:
create table t_user(
`id` int(10) AUTO_INCREMENT PRIMARY KEY,
`username` VARCHAR(25) ,
`password` VARCHAR(25)
) ENGINE=INNODB;
--插入两条数据方便测试
insert into t_user(username,password) values('curry','123456')
insert into t_user(username,password) values('rose','123456')