JDBC【4】-- jdbc預編譯與拼接sql對比
- 在jdbc中,有三種方式執行sql,分別是使用Statement(sql拼接),PreparedStatement(預編譯),還有一種CallableStatement(儲存過程),在這裡我就不介紹CallableStatement了,我們來看看Statement與PreparedStatement的區別。
1. 建立資料庫,資料表
資料庫名字是test,資料表的名字是student,裡面有四個欄位,一個是id,也就是主鍵(自動遞增),還有名字,年齡,成績。最後先使用sql語句插入六個測試記錄。
CREATE DATABASE `test` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; CREATE TABLE `student` ( `id` INT NOT NULL AUTO_INCREMENT , `name` VARCHAR(20) NOT NULL , `age` INT NOT NULL , `score` DOUBLE NOT NULL , PRIMARY KEY (`id`)) ENGINE = MyISAM; INSERT INTO `student` VALUES (1, '小紅', 26, 83); INSERT INTO `student` VALUES (2, '小白', 23, 93); INSERT INTO `student` VALUES (3, '小明', 34, 45); INSERT INTO `student` VALUES (4, '張三', 12, 78); INSERT INTO `student` VALUES (5, '李四', 33, 96); INSERT INTO `student` VALUES (6, '魏紅', 23, 46);
建立對應的學生類:
/** * student類,欄位包括id,name,age,score * 實現無參構造,帶參構造,toString方法,以及get,set方法 * @author 秦懷 */ public class Student { private int id; private String name; private int age; private double score; public Student() { super(); // TODO Auto-generated constructor stub } public Student(String name, int age, double score) { super(); this.name = name; this.age = age; this.score = score; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public double getScore() { return score; } public void setScore(double score) { this.score = score; } @Override public String toString() { return "Student [id=" + id + ", name=" + name + ", age=" + age + ", score=" + score + "]"; } }
2.Statement
先來看程式碼,下面是獲取資料庫連線的工具類 DBUtil.class:
public class DBUtil { private static String URL="jdbc:mysql://127.0.0.1:3306/test"; private static String USER="root"; private static String PASSWROD ="123456"; private static Connection connection=null; static{ try { Class.forName("com.mysql.jdbc.Driver"); // 獲取資料庫連線 connection=DriverManager.getConnection(URL,USER,PASSWROD); System.out.println("連線成功"); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } // 返回資料庫連線 public static Connection getConnection(){ return connection; } }
下面是根據id查詢學生資訊的程式碼片段,返回student物件就能輸出了:
public Student selectStudentByStatement(int id){
// 拼接sql語句
String sql ="select * from student where id = "+id;
try {
// 獲取statement物件
Statement statement = DBUtil.getConnection().createStatement();
// 執行sql語句,返回 ResultSet
ResultSet resultSet = statement.executeQuery(sql);
Student student = new Student();
// 一條也只能使用resultset來接收
while(resultSet.next()){
student.setId(resultSet.getInt("id"));
student.setName(resultSet.getString("name"));
student.setAge(resultSet.getInt("age"));
student.setScore(resultSet.getDouble("score"));
}
return student;
} catch (SQLException e) {
// TODO: handle exception
}
return null;
}
我們可以看到整個流程是先獲取到資料庫的連線Class.forName("com.mysql.jdbc.Driver"); connection=DriverManager.getConnection(URL,USER,PASSWROD);
獲取到連線之後通過連接獲取statement物件,通過statement來執行sql語句,返回resultset這個結果集,Statement statement = DBUtil.getConnection().createStatement();ResultSet resultSet = statement.executeQuery(sql);
,值得注意的是,上面的sql是已經拼接好,寫固定了的sql,所以很容易被注入,比如這句:
sql = "select * from user where name= '" + name + "' and password= '" + password+"'";
如果有人
- name = "name' or '1'= `1"
- password = "password' or '1'='1",那麼整個語句就會變成:
sql = "select * from user where name= 'name' or '1'='1' and password= 'password' or '1'='1'";
那麼就會返回所有的資訊,所以這是很危險的。
還有更加危險的,是在後面加上刪除表格的操作,不過一般我們都不會把這些許可權開放的。
// 如果password = " ';drop table user;select * from user where '1'= '1"
// 後面一句不會執行,但是這已經可以刪除表格了
sql = "select * from user where name= 'name' or '1'='1' and password= '' ;drop table user;select * from user where '1'= '1'";
所以預編譯顯得尤為重要了。
3.PreparedStatement預編譯
我們先來看看預編譯的程式碼:
// 根據id查詢學生
public Student selectStudent(int id){
String sql ="select * from student where id =?";
try {
PreparedStatement preparedStatement = DBUtil.getConnection()..prepareStatement(sql);
preparedStatement.setInt(1, id);
ResultSet resultSet = preparedStatement.executeQuery();
Student student = new Student();
// 一條也只能使用resultset來接收
while(resultSet.next()){
student.setId(resultSet.getInt("id"));
student.setName(resultSet.getString("name"));
student.setAge(resultSet.getInt("age"));
student.setScore(resultSet.getDouble("score"));
}
return student;
} catch (SQLException e) {
// TODO: handle exception
}
return null;
}
預編譯也是同樣需要獲取到資料庫連線物件connection,但是sql語句拼接的時候使用了佔位符?,將含有佔位符的sql當引數傳進去,獲取到PreparedStatement預編譯的物件,最後是通過set來繫結引數,然後再去使用execute執行預編譯過的程式碼。這樣就避免了sql注入的問題,同時,由於sql已經編譯過快取在資料庫中,所以執行起來不用再編譯,速度就會比較快。
4.為什麼預編譯可以防止sql注入
- 在使用佔位符,或者說引數的時候,資料庫已經將sql指令編譯過,那麼查詢的格式已經訂好了,也就是我們說的我已經明白你要做什麼了,你要是將不合法的引數傳進去,會有合法性檢查,使用者只需要提供引數給我,引數不會當成指令部分來執行,也就是預編譯已經把指令以及引數部分割槽分開,引數部分不允許傳指令進來。
這樣的好處查詢速度提高,因為有了預編譯快取,方便維護,可讀性增強,不會有很多單引號雙引號,容易出錯,防止大部分的sql注入,因為引數和sql指令部分資料庫系統已經區分開。百度文庫裡面提到:傳遞給PreparedStatement物件的引數可以被強制進行型別轉換,使開發人員可以確保在插入或查詢資料時與底層的資料庫格式匹配。
要是理解不透徹可以這麼來理解:
select * from student where name= ?
預編譯的時候是先把這句話編譯了,生成sql模板,相當於生成了一個我知道你要查名字了,你把名字傳給我,你現在想耍點小聰明,把字串'Jame' or '1=1'
傳進去,你以為他會變成下面這樣麼:
select * from student where name= 'Jame' or '1=1'
放心吧,不可能的,這輩子都不可能的啦,資料庫都知道你要幹嘛了,我不是有sql模板了麼,資料庫的心裡想的是我叫你傳名字給我,行,這名字有點長,想害我,可以,我幫你找,那麼資料庫去名字這一欄位幫你找一個叫'Jame' or '1=1'
的人,他心裡想這人真逗,沒有這個人,沒有!!!
所以這也就是為什麼預編譯可以防止sql注入的解釋了,它是經過了直譯器解釋過的,解釋的過程我就不囉嗦了,只要是對引數做轉義,轉義之後讓它在拼接時只能表示字串,不能變成查詢語句。
此文章僅代表自己(本菜鳥)學習積累記錄,或者學習筆記,如有侵權,請聯絡作者刪除。人無完人,文章也一樣,文筆稚嫩,在下不才,勿噴,如果有錯誤之處,還望指出,感激不盡~
技術之路不在一時,山高水長,縱使緩慢,馳而不息。
公眾號:秦懷雜貨店