sql注入原理
用户输入的信息中含有sql语句的关键字,并且这些关键字参与sql语句编译过程,导致sql语句的原意被扭曲,进而达到sql注入。
sql注入的例子
存在SQL注入的代码:
public class Test {
public static void main(String[] args) {
// 初始化一个界面
Map<String,String> userLoginInfo = initUI();
// 验证用户名和密码
boolean loginSuccess = login(userLoginInfo);
// 最后输出结果
System.out.println(loginSuccess ? "登录成功" : "登录失败");
}
/**
* 用户登录
* @param userLoginInfo 用户登录信息
* @return false表示登录失败,true表示登录成功
*/
private static boolean login(Map<String, String> userLoginInfo) {
// JDBC
boolean loginSuccess = false;
Connection connection = null;
Statement statement = null;
ResultSet rs = null;
String loginName = userLoginInfo.get("loginName");
String loginPwd = userLoginInfo.get("loginPwd");
try {
// 1、注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2、获取连接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/MyTest","root","******");
// 3、获取数据库操作对象
statement = connection.createStatement();
// 4、执行sql语句
String sql = "select * from tbl_user where loginName = '" + loginName + "' and loginPwd = '" + loginPwd + "'";
rs = statement.executeQuery(sql);
// 5、处理结果集
if(rs.next()){
loginSuccess = true;
}
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
} finally {
// 6、释放资源
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
return loginSuccess;
}
/**
* 初始化用户界面
* @return 用户输入的用户名和密码等信息
*/
private static Map<String, String> initUI() {
Scanner s = new Scanner(System.in);
System.out.print("用户名:");
String loginName = s.nextLine();
System.out.print("密码:");
String loginPwd = s.nextLine();
Map<String,String> userLoginInfo = new HashMap<>();
userLoginInfo.put("loginName", loginName);
userLoginInfo.put("loginPwd", loginPwd);
return userLoginInfo;
}
}
当输入如下时,密码即使不正确,也能登录成功:
可以通过断点查看,发现实际sql语句中多出了 or '1'='1',导致这条sql语句永远能查到数据
sql注入的解决方法
只要用户提供的信息不参与SQL语句的编译过程,问题就解决了。要想用户提供的信息不参与SQL语句的编译,那么必须使用java.sql.PreparedStatement
PreparedStatement接口继承了java.sql.Statement
PreparedStatement是属于预编译的数据库操作对象
PreparedStatement的原理是:预先对SQL语句的框架进行编译,然后再给SQL语句传“值”
使用方法:
String sql = "select * from tbl_user where loginName = ? and loginPwd = ?"; ps = connection.prepareStatement(sql);注:
(1)SQL语句的框子,其中一个?,表示一个占位符,一个?将来接受一个“值”,注意:占位符不能用单引号括起来。
(2)给占位符?传值(第一个问号下标是1,第二个问号下标是2,JDBC所有下标从1开始)
setString(占位符下标,String)
通过PreparedStatement接口改良上方代码
解决了SQL注入的问题
public static void main(String[] args) {
// 初始化一个界面
Map<String,String> userLoginInfo = initUI();
// 验证用户名和密码
boolean loginSuccess = login(userLoginInfo);
// 最后输出结果
System.out.println(loginSuccess ? "登录成功" : "登录失败");
}
/**
* 用户登录
* @param userLoginInfo 用户登录信息
* @return false表示登录失败,true表示登录成功
*/
private static boolean login(Map<String, String> userLoginInfo) {
// JDBC
boolean loginSuccess = false;
Connection connection = null;
PreparedStatement ps = null;
ResultSet rs = null;
String loginName = userLoginInfo.get("loginName");
String loginPwd = userLoginInfo.get("loginPwd");
try {
// 1、注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2、获取连接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/MyTest","root","jht123456");
// 3、获取预编译的数据库操作对象
// ?表示占位符,不能写成'?',否则编译器认为是普通的字符串
String sql = "select * from tbl_user where loginName = ? and loginPwd = ?";
ps = connection.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;
}
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
} finally {
// 6、释放资源
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (ps != null) {
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
return loginSuccess;
}
/**
* 初始化用户界面
* @return 用户输入的用户名和密码等信息
*/
private static Map<String, String> initUI() {
Scanner s = new Scanner(System.in);
System.out.print("用户名:");
String loginName = s.nextLine();
System.out.print("密码:");
String loginPwd = s.nextLine();
Map<String,String> userLoginInfo = new HashMap<>();
userLoginInfo.put("loginName", loginName);
userLoginInfo.put("loginPwd", loginPwd);
return userLoginInfo;
}
Statement和PreparedStatement的对比
1、Statement会存在SQL注入问题,PreparedStatement解决了这个问题。
2、Statement是编译一次执行一次。PreparedStatement是编译一次,可执行N次。相比之下PreparedStatement效率较高。
3、PreparedStatement会在编译阶段进行类型的安全检查。
4、综上所述,PreparedStatement使用较多,只有极少数的情况下需要使用Statement,只有必须要求SQL注入的情况下,才使用Statement。
必须使用Statement的情况
当sql语句中必须用到字符串拼接时,则必须使用Statement
public static void main(String[] args) {
Scanner s = new Scanner(System.in);
System.out.print("升序输入asc,降序输入desc:");
String order = s.nextLine();
// 定义变量
Connection connection = null;
Statement statement = null;
ResultSet rs = null;
try {
// 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 获取连接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/MyTest","root", "********");
// 获取数据库操作对象
statement = connection.createStatement();
// 执行sql
String sql = "select * from emp order by sal " + order;
rs = statement.executeQuery(sql);
// 处理查询结果集
while(rs.next()){
String ename = rs.getString("ename");
double sal = rs.getDouble("sal");
System.out.println("姓名:" + ename + ",薪资:" + sal);
}
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
} finally {
// 释放资源
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}