在JDBC中使用PreparedStatement代替Statement,同時預防SQL注入


  本篇講訴為何在JDBC操作數據庫的過程中,要使用PreparedStatement對象來代替Statement對象。

  在前面的JDBC學習中,對於Statement對象,我們已經知道是封裝SQL語句並發送給數據庫執行的功能,但是實際開發中,這個功能我們更經常用的是Statement類的子類PreparedStatement類的對象來實現,而不是采用Statement對象。

  Statement和PreparedStatement的關系與區別在於:

  ① PreparedStatement類是Statement類的子類,擁有更多強大的功能。

  ② PreparedStatement類可以防止SQL注入攻擊的問題,后面會說到。

  ③ PreparedStatement會對SQL語句進行預編譯,以減輕數據庫服務器的壓力,而Statement則無法做到。

 

例1 :使用PreparedStatement對代碼中Statement進行更換

  構建一張user表,接着我們要在程序中使用JDBC對數據庫進行User對象數據的添加,先用Statement對象來展示,后使用PreparedStatement對象,以此來比較兩者的不同。

  在MySQL數據庫中新建jdbcdemo庫,並構建user表:

1:創建一個數據庫 create database jdbcdemo; 2:使用剛創建的數據庫 use jdbcdemo; 3:創建一個user表 create table user( id int primary key, name varchar(40), age int );

  創建工程,在工程中導入數據庫連接驅動的jar包。在【src】目錄下新建一個database.properties文件,內容如下:

    driver=com.mysql.jdbc.Driver
    url=jdbc:mysql://localhost:3306/jdbcdemo
    username=root
    password=root

  構建JDBC的工具類,包括注冊驅動,獲取連接,釋放資源和連接等,這部分同《JDBC操作數據庫的學習(2)》中相同,代碼如下:

 1 public class JdbcUtils {  2     
 3     private static Properties config = new Properties();  4     
 5     static{  6         InputStream in = JdbcUtils.class.getClassLoader().getResourceAsStream("database.properties");  7         try{  8  config.load(in);  9             Class.forName(config.getProperty("driver")); 10             
11         
12         }catch (Exception e) { 13             throw new ExceptionInInitializerError(e); 14  } 15  } 16     
17     public static Connection getConnection() throws SQLException { 18         String url = config.getProperty("url"); 19         String username = config.getProperty("username"); 20         String password = config.getProperty("password"); 21         Connection conn = DriverManager.getConnection(url, username, password); 22         return conn; 23  } 24     
25     public static void release(Connection conn,Statement st,ResultSet rs) { 26         if(rs!=null) { 27             try{ 28  rs.close(); 29             }catch (Exception e) { 30  e.printStackTrace(); 31  } 32  } 33         if(st!=null) { 34             try{ 35  st.close(); 36             }catch (Exception e) { 37  e.printStackTrace(); 38  } 39  } 40         if(conn!=null) { 41             try{ 42  conn.close(); 43             }catch (Exception e) { 44  e.printStackTrace(); 45  } 46  } 47  } 48 }
View Code

  要添加User對象,那必須在工程中定義的domain包中做出User類的JavaBean:

1 public class User { 2     private int id; 3     private String name; 4     private int age; 5     。。。//省略getter方法和setter方法
6 }

  回歸正題,例如我們需要在UserDao層的實現類UserDaoImpl中對User對象進行數據庫的增刪改查,這里僅展示對User對象的添加,先使用Statement對象:

 1 public class UserDaoImpl {  2     public void insert(User user) throws SQLException {  3         Connection conn = null;  4         Statement st = null;  5         ResultSet rs = null;  6         
 7         try{  8             conn = JdbcUtils.getConnection();  9             st = conn.createStatement(); 10             String sql = "insert into user(id,name,age) values("+user.getId()+",'"+user.getName()+"','"+user.getAge()+"')"; //這里簡直讓人奔潰
11             int num = st.executeUpdate(sql); 12             if(num>0){ 13                 System.out.println("添加成功"); 14             }else{ 15                 System.out.println("添加失敗"); 16  } 17         }finally{ 18  JdbcUtils.release(conn, st, rs); 19  } 20  } 21 }
View Code

 

-------------------------------------我是使用PreparedStatement的分割線-------------------------------

  使用PreparedStatement對上面例子的代碼進行更改:

 1 public class UserDaoImpl {  2     public void insert(User user) throws SQLException {  3         Connection conn = null;  4         PreparedStatement st = null;  5         ResultSet rs = null;  6         try{  7             conn = JdbcUtils.getConnection();  8             String sql = "insert into user(id,name,age) values(?,?,?)";  9             st = conn.prepareStatement(sql); 10             st.setInt(1, user.getId()); 11             st.setString(2, user.getName()); 12             st.setInt(3, user.getAge()); 13             int num = st.executeUpdate(); 14             if(num>0){ 15                 System.out.println("添加成功"); 16             }else{ 17                 System.out.println("添加失敗"); 18  } 19             
20         }finally{ 21  JdbcUtils.release(conn, st, rs); 22  } 23  } 24 }
View Code

  在上面的代碼中,使用PreparedStatement與Statement代碼不同的地方都已經用紅字標出:

  ⑴ 首先我們在封裝sql語句的對象應該使用PreparedStatement。

  ⑵ 我們在SQL語句的字符串中,以問號“?”來替代數據,相當於在國際化中的占位符,可以看到如果使用Statement的SQL語句字符串,要不斷地使用字符串連接符,整個SQL字符串看上去簡直讓人奔潰。

      ⑶ 使用Connection對象的prepareStatement(String  sql)語句來創建PreparedStatement對象,在創建的工程中就要傳入sql語句作為參數,這點和Statement不同。

  ⑷ 通過PreparedStatement對象的setXXX方法來對SQL語句字符串中的占位符進行替換,根據在數據庫中的類型不同而采用不同的set方法。通過這種方式,我們可以 清晰地看清楚在SQL語句中每個占位符和一一對應在數據庫中的列數據。

  ⑸ 最后使用PreparedStatement的executeUpdate()方法發送給數據庫執行,注意和Statement不同,這里無需再向executeUpdate方法中傳入參數,因為在創建PreparedStatement對象時已經傳入了SQL字符串語句。如果使用查詢executeQuery方法也是無需傳入參數。

 

  通過上面的例子我們可以看到使用PreparedStatement在編寫JDBC操作數據庫的SQL字符串語句會非常好用,也使對應的數據顯得非常清楚。

  不僅如此,PreparedStatement是能極大的減輕數據庫服務器的壓力的。從PreparedStatement的名字來看是叫“預編譯”,如果要解釋為什么預編譯能優化數據庫,那么就要從數據庫說起,在操作數據庫時,我們的每一條SQL語句都要在數據庫中進行編譯並執行,這一點和程序是類似的。對於JDBC來說,使用Statement對象那么只對SQL語句進行封裝然后發送給數據庫編譯並執行,如果對數據庫操作頻繁,那么數據庫都要做編譯和執行兩個步驟;而使用PreparedStatement對象的話,則由程序對SQL語句先進行編譯,然后再發送給數據庫服務器執行,這樣就極大地減輕了數據庫服務器的壓力。

  除了能給數據庫服務器帶來極大的優化作用之外,PreparedStatement另外一個極大的功能就在於能防止SQL注入攻擊。

  所謂的SQL注入,就是把SQL語句輸入到WEB表單、或者輸入域名或頁面請求的查詢字符串等等,在請求發送給服務器的過程中, 達到欺騙數據庫服務器執行惡意的SQL命令。

 

例2:使用SQL注入進行用戶登錄

  要使SQL注入能成功,必須得是Statement對象才行。

  還是以例1創建user表為案例,如果我們開發好web層,做好前端頁面顯示,比如用戶登錄,就需要填寫用戶登錄的用戶名,而這里用戶名就可以用惡意的SQL語句,假設我們在該處填寫的用戶名為:  ' or 1=1 or name='

  在點擊提交或者登錄按鈕后,會以表單形式從web工程中層層傳下,到dao層將表單的數據進行操作數據庫,以匹配是否存在該用戶,在dao層對User對象操作的UserDaoImpl實現類中,查找用戶的代碼如下:

 1 public class UserDaoImpl {  2 public User find(String name) throws SQLException {  3         Connection conn = null;  4         Statement st = null;  5         ResultSet rs = null;  6         try{  7             conn = JdbcUtils.getConnection();  8             String sql = "select * from user where name='" +name+ "'";  9             st = conn.createStatement(); 10             rs = st.executeQuery(sql); 11             if(rs.next()) { 12                 User user = new User(); 13                 user.setId(rs.getInt("id")); 14                 user.setName(rs.getString("name")); 15                 user.setAge(rs.getInt("age")); 16                 return user; 17             }else{ 18                 return null; 19  } 20         }finally{ 21  JdbcUtils.release(conn, st, rs); 22  } 23  } 24 }
View Code

  通過上述代碼,經過測試,根據頁面帶過來的表單數據name值為' or 1=1 or name='是可以進行用戶登錄的。

 

  分析:我們若將表單填寫的數據帶到代碼中的SQL語句,就形成如下的SQL命令:

  select * from user where name='' or 1=1 or name=''

      可以看到使用Statement對象就是將兩個字符串拼接形成的SQL語句,這樣做很可能會將判斷條件改變,如上面的命令,在where語句中出現了or  1=1 這樣一定會返回true的語句,就如同程序發送一條“select * from user where true”的句子,那么數據庫執行這條語句根本不需要篩選條件,只要數據庫有任意用戶,都可以告訴程序你找到了該指定用戶,那么我們連密碼都不用填的只需要惡意SQL語句即可登錄網站。這就是一個SQL注入的典型例子。

  而使用PreparedStatement則不會,因為PreparedStatement的預編譯,會將表單中所填寫的數據進行編譯,這種編譯是包含字符過濾的編譯,就好像對html進行過濾轉義一樣,這字符過濾最關鍵的因素在於PreparedStatement使用的是占位符,而不會像Statement那樣因為拼接字符串而引入了引號,可以看到在PreparedStatement中即使接收的表單數據中SQL語句以引號包圍,由於程序中的SQL語句使用占位符,因此就相當於條件為where name=' or 1=1 or name=',顯然數據庫並沒有這樣的記錄,因此防止了SQL注入的問題。

  關於SQL注入也算是一門有趣的學問,建議平時對這方面多學習些,也有利於我們更好地加強自己開發中的安全性。

 

 

    

    


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM