讓我們仔細看看是怎麼訪問資料庫的
package sql;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Conn { // 建立類Conn
Connection con; // 宣告Connection物件
public static String user;
public static String password;
public Connection getConnection() { // 建立返回值為Connection的方法
try { // 載入資料庫驅動類
Class.forName("com.mysql.cj.jdbc.Driver");
System.out.println("資料庫驅動載入成功");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
user = "root";//資料庫登入名
password = "root";//密碼
try { // 透過訪問資料庫的URL獲取資料庫連線物件
con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test1?useUnicode=true&characterEncoding=gbk", user, password);
System.out.println("資料庫連線成功");
} catch (SQLException e) {
e.printStackTrace();
}
return con; // 按方法要求返回一個Connection物件
}
public static void main(String[] args) { // 主方法,測試連線
Conn c = new Conn(); // 建立本類物件
c.getConnection(); // 呼叫連線資料庫的方法
}
}
具體用法
我們直接看下列的程式碼
package Main;
import java.sql.*;
public class JDBC {
public static void main(String[] args) throws SQLException, ClassNotFoundException {
// 1.載入驅動
Class.forName("com.mysql.cj.jdbc.Driver");
// 2.使用者資訊和url
String url = "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=true";
String username="root";
String password="root";
// 3.連線成功,資料庫物件 Connection
Connection connection = DriverManager.getConnection(url,username,password);
// 4.執行SQL物件Statement,執行SQL的物件
Statement statement = connection.createStatement();
// 5.執行SQL的物件去執行SQL,返回結果集
String sql = "SELECT *FROM studentinfo;";
ResultSet resultSet = statement.executeQuery(sql);
while(resultSet.next()){
System.out.println("SNo="+resultSet.getString("SNo"));
System.out.println("SName="+resultSet.getString("SName"));
System.out.println("Birth="+resultSet.getString("Birth"));
System.out.println("SPNo="+resultSet.getString("SPNo"));
System.out.println("Major="+resultSet.getString("Major"));
System.out.println("Grade="+resultSet.getString("Grade"));
System.out.println("SInstructor="+resultSet.getString("SInstructor"));
System.out.println("SPwd="+resultSet.getString("SPwd"));
}
// 6.釋放連線
resultSet.close();
statement.close();
connection.close();
}
}
載入資料庫類
Class.forName("com.mysql.cj.jdbc.Driver");
是用來載入資料庫驅動的。用於我們的 Java 程式與資料庫通訊。
Class.forName()
函式的作用是用於動態載入一個類,其中的引數自然也是資料庫的驅動類com.mysql.cj.jdbc.Driver
連線資料庫
我們通常使用DriverManager.getConnection(url,username,passwd)
方法來連線資料庫,這裡面需要我們填入三個引數:
-
url:資料庫的URL,格式很重要
-
"jdbc:mysql"
:這是告訴程式使用 JDBC 驅動來連線 MySQL 資料庫。 -
"localhost"
:指向本地計算機的資料庫伺服器。 -
"3306"
:MySQL 服務的預設埠號。 -
"test1"
:這是要訪問的資料庫名稱。 -
?useUnicode=true&characterEncoding=gbk
:這些是查詢引數,用於指定資料庫的配置。它們表示: -
useUnicode=true
:啟用 Unicode 支援,確保可以儲存和讀取 Unicode 字元。 -
characterEncoding=gbk
:設定字元編碼為 GBK,通常用於處理中文字元。
-
-
username:資料庫的使用者名稱
-
passwd :資料庫的密碼
例項化一個SQL物件Statement
這沒啥好說的,就是例項化一個物件,以便於我們能呼叫其中的各種方法
執行SQL語句,查詢資料庫
查詢
要執行資料庫的查詢我們直接使用executeQuery(String sql)
方法,然後裡面寫入我們的sql語句就行,之後我們就能從返回值得到查詢的結果了ResultSet resultSet = statement.executeQuery(sql);
結果
得到了一個ResultSet
結果物件之後,想要的得到字串直接使用getString()
方法就行,除此之外還有getLong()
,getInt()
等,就對應了其中的資料型別。
然後就是這些get方法的引數,有兩種引數:
- 列名稱:就是你查詢出來後,直接輸入列表的名稱就給你輸出了查詢的結果,名稱的型別自然就是String型別的
- 列索引:直接從中輸入列的索引就會輸入第幾列的查詢結果,但是注意!!!!這裡的所以不再是從0開始,而是從1開始,這是非常值得注意的地方,所以的引數型別自然就是int了
釋放連結
為了資源不要浪費,使用完了就應該直接釋放了
resultSet.close();
statement.close();
connection.close();
防止sql注入的改良
發現問題
又細心的人就會發現前面的程式碼使用Statement
拼字串非常容易引發SQL隱碼攻擊的問題。
什麼是SQL隱碼攻擊呢?我們一般查詢資料庫靠著相對的命令來實現,如果SQL語句是靠要查的字元拼接出來的,一般是沒有問題的,但是我們輸入一些特定的字元的時候是可能會讓sql語句去做其他的事情。總之SQL一般都是因為字元的拼接漏洞實現的。
解決問題
所以我們就得想辦法去解決這個問題,有一個方法是轉義特定的字元,但這終究是治標不治本的。
前面我們提到,最根本的問題是字元拼接帶來的漏洞,如果我們不進行字元拼接,直接傳遞要查的字元,那問題就引刃而解了
把 Statement
換成 PreparedStatement
可以完全避免SQL隱碼攻擊的問題,因為PreparedStatement
始終使用?
作為佔位符,並且把資料連同SQL本身傳給資料庫,這樣可以保證每次傳給資料庫的SQL語句是相同的,只是佔位符的資料不同,還能高效利用資料庫本身對查詢的快取。
//使用prepareStatement查詢
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
try (PreparedStatement ps = conn.prepareStatement("SELECT id, grade, name, gender FROM students WHERE gender=? AND grade=?")) {
ps.setObject(1, "M"); // 注意:索引從1開始
ps.setObject(2, 3);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
long id = rs.getLong("id");
long grade = rs.getLong("grade");
String name = rs.getString("name");
String gender = rs.getString("gender");
}
}
}
}
//使用Statement查詢
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
try (Statement stmt = conn.createStatement()) {
try (ResultSet rs = stmt.executeQuery("SELECT id, grade, name, gender FROM students WHERE gender=1")) {
while (rs.next()) {
long id = rs.getLong(1); // 注意:索引從1開始
long grade = rs.getLong(2);
String name = rs.getString(3);
int gender = rs.getInt(4);
}
}
}
}
我們來看看這個例子,上面的是使用的prepareStatement,下面使用的是Statement。雖然兩者之間沒有太大的區別,但是還有值得我們注意的地方:
- sql語句插入的函式不同,前者是在prepareStatement就已經插入,而後者是在executeQuery才插入
- prepareStatement是需要使用setObject方法來指定我們查詢的字元的,但是Statement是不用的,至於為什麼不行我們後文再說
ResultSet
的next()
方法用於 移動遊標 到 結果集中的下一行,返回的資料型別看程式碼無疑是一個boolean
值
解決完問題帶來的思考
我們從程式碼層面分析完成之後,我想很多人跟我有一樣的提問,prepareStatement
為什麼能夠做到防止SQL的注入,這裡我們在稍微升入SQL隱碼攻擊一點,在詳細剖析SQL隱碼攻擊是怎麼完成的。
SQL隱碼攻擊的本質
SQL隱碼攻擊漏洞出現的原因就是使用者的輸入會直接嵌入到查詢語句中,一旦出現精心設計的輸入就會改變整個SQL語句的結構
比如現在有這樣的語句
String query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
如果輸入了惡意的內容 username = "admin' --"
SELECT * FROM users WHERE username = 'admin' --' AND password = 'password';
後面輸入的AND password = 'password';
就直接被註釋掉了,這樣就會只查詢前面的username = 'admin'
prepareStatement
的防禦原理
前面不是說了就是因為使用者輸入和查詢語句不是分離的嗎,那思路就很簡單了,那將兩者分離不就行了
佔位符分離
在預編譯階段,SQL 查詢的結構被解析併傳送到資料庫中,這時 佔位符(?
)會被資料庫視為引數的佔位符,而不是 SQL 語句的一部分。
例如:
String query = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement ps = conn.prepareStatement(query);
ps.setString(1, username);
ps.setString(2, password);
在這裡:
?
是佔位符,它在查詢執行時被替換為實際的引數。ps.setString(1, username)
和ps.setString(2, password)
將使用者輸入的值安全地繫結到查詢中。- 在執行查詢時,資料庫知道
?
只是佔位符,它不會將使用者輸入作為 SQL 程式碼的一部分來解析,而是將其作為資料處理。
即使使用者輸入惡意的內容,例如:
username = "admin' OR 1=1 --"
password = "password"
構造的 SQL 查詢也不會發生注入,因為資料庫會將這些輸入當作普通的字串處理,而不會將其作為 SQL 語句的一部分。執行時的 SQL 查詢將是:
SELECT * FROM users WHERE username = 'admin'' OR 1=1 --' AND password = 'password'
這個查詢在資料庫端仍然會被正確地作為兩條字串值傳遞,而不會被解析為惡意的 SQL 程式碼
自動轉譯使用者輸入
PreparedStatement
會自動轉義引數中的特殊字元,如單引號('
)等,使其在資料庫中正確地作為字串處理。這進一步防止了 SQL 注入攻擊。
例如,如果使用者輸入的使用者名稱是:
admin' --
PreparedStatement
會自動將這個字串轉義為:
'admin'' --'
這樣,即使使用者輸入惡意的內容,資料庫也會將其作為普通字串處理,而不會被當作 SQL 語句的一部分執行。