sql注入攻击指的是通过构建特殊的输入作为参数传入web应用程序,而这些输入大都是sql语法里的一些组合,通过执行sql语句进而执行攻击者所要做的操作,其产生的主要原因在于应用程序没有细致地过滤用户输入的数据,致使非法数据侵入系统。
我们通过一个用户登录的小案例来大致了解sql注入的情况:(Oracle数据库环境)
首先准备一张表t_user:
create table t_user(
name varchar2(10) primary key,
password varchar2(10) not null
)
插入一些测试数据:
insert into t_user(name,password) values('tom','123');
insert into t_user(name,password) values('mary','456');
commit;
所使用的数据库驱动:ojdbc8.jar
用JDBC连接数据库,写一个登录案例:
public class TestSqlInjection {
public static void main(String[] args) {
String driverClass = "oracle.jdbc.OracleDriver";
String url = "jdbc:oracle:thin:@127.0.0.1:1521:XE";
String user = "briup";
String password = "briup";
@SuppressWarnings("resource")
Scanner input = new Scanner(System.in);
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
Class.forName(driverClass);
conn = DriverManager.getConnection(url, user, password);
stmt = conn.createStatement();
System.out.println("请输入用户名:");
String uname = input.nextLine();
System.out.println("请输入密码:");
String upass = input.nextLine();
String sql = "SELECT name,password FROM t_user WHERE name='" + uname + "' AND password='" + upass + "'";
// uname xxx
// upass xxx' or '1'='1
rs = stmt.executeQuery(sql);
if (rs.next()) {
System.out.println("登录成功");
// 这时候我们认为用户名和密码都正确情况下查询出来的用户肯定只有一条
System.out.println("欢迎您," + uname);
} else {
System.err.println("用户名或密码错误!");
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
效果:
很显然使用Statement的确产生了sql注入的问题。
但是使用PreparedStatement
可以有效解决这个问题。
public class TestSqlInjection {
public static void main(String[] args) {
String driverClass = "oracle.jdbc.OracleDriver";
String url = "jdbc:oracle:thin:@127.0.0.1:1521:XE";
String user = "briup";
String password = "briup";
@SuppressWarnings("resource")
Scanner input = new Scanner(System.in);
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
Class.forName(driverClass);
conn = DriverManager.getConnection(url, user, password);
String sql = "SELECT name,password FROM t_user WHERE name=? AND password=?";
ps = conn.prepareStatement(sql);
//uname xxx
//upass xxx' or '1'='1
System.out.println("请输入用户名:");
String uname = input.nextLine();
ps.setString(1, uname);
System.out.println("请输入密码:");
String upass = input.nextLine();
ps.setString(2, upass);
rs = ps.executeQuery();
if (rs.next()) {
System.out.println("登录成功");
System.out.println("欢迎您," + uname);
} else {
System.err.println("用户名或密码错误!");
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
PreparedStatement 除了可以防止sql注入,在执行大量重复性插入语句时,也会明显提升程序运行的性能,同时避免了繁琐的字符串拼接操作。
对比:
- Statement ,是每次执行一个sql语句,就要把一个完成的sql语句发送给数据库进行执行,然后取回返回的结果。
- PreparedStatement ,可以把一个sql语句的结构,提前发送给数据库进行预处理,然后在专门给发送要操作的具体的值,在数据量大的时候,这种方式会大大提高执行效率。