Hướng dẫn dùng prepared statements trong PHP

Nội Dung

  • 1. Prepared statements là gì?
  • 2. Cơ chế prepared SQL và quy trình thực hiện
  • 3. Sử dụng Prepared Statement bằng MySQLi
    • 3.1. Sử dụng MySQLi hướng thủ tục
    • 3.2. Sử dụng MySQLi hướng đối tượng
  • 4. Sử dụng Prepared Statement bằng PDO
  • 5. Ưu điểm của việc sử dụng Prepared Statement

Prepared statements hay còn gọi là câu lệnh được tham số hóa thực chất chỉ đơn giản là một câu lệnh truy vấn SQL có chứa trình giữ chỗ thay vì các giá trị tham số thực tế. Các trình giữ chỗ này sẽ được thay thế bằng các giá trị thực tại thời điểm thực thi câu lệnh.

Ở một số website code bằng PHP thuần thường mắc phải lỗi SQL injection là do lập trình viên không biết đến lỗi này hoặc biết nhưng không tìm cách khắc phục nó. Trước đây chúng ta hay sử dụng hàm addslashes() trong PHP để khắc phục nhưng bây giờ các thư viện xử lý database cũng đã đưa ra cơ chế prepared nhằm hạn chế lỗi bảo mật này.

2. Cơ chế prepared SQL và quy trình thực hiện

Mình sẽ đưa ra cơ chế hoạt động như sau: khi chúng ta viết câu truy vấn mà có dữ liệu động thì thay vì truyền trực tiếp tham số thì chúng ta sẽ thay các tham số đó bằng các biến ẩn danh, rồi sau đó chúng ta sẽ truyền các giá trị cho các biến ẩn danh đó và PHP sẽ prepared sao cho bảo mật nhất rồi mới chạy câu truy vấn.

Trước đây nếu truyền tham số vào câu truy vấn thì chúng ta hay thực hiện như ví dụ dưới đây:

$username = $_POST['username'];
$password = $_POST['password']; 
$sql = "select * from users where username = '$username' and password = '$password'";

Đây là cách gán chuỗi thông thường và sẽ mắc phải lỗi SQL injection. Ta có một cách khác sẽ khắc phục được phần nào đó là sử dụng hàm addslashes():

$username = addslashes($_POST['username']);
$password = addslashes($_POST['password']);
$sql = "select * from users where username = '$username' and password = '$password'";

Bây giờ người ta đã không sử dụng cách trên nữa mà sử dụng chức năng có sẵn trong các thư viện MySQLi và PDO. Mỗi thư viện có cách thể hiện khác nhau nhưng chúng đều có chung một quy tắc đó là:

  • Chúng ta đưa vào một câu truy vấn với các tham số là một ẩn danh
  • Chúng ta truyền vào giá trị tương ứng cho các ẩn danh đó
  • PHP sẽ dựa vào thư tự các tham số ẩn danh và các giá trị để repared sao bảo mật nhất.
  • Cuối cùng sẽ thực thi câu truy vấn.
  • Khi bạn đã khai báo các tham số lần đầu rồi và sau đó muốn sử dụng tiếp thì không cần phải khai báo nữa. Đây cũng chính là lợi thế của cơ chế prepared trong PHP.

Việc thực hiện Prepared Statement bao gồm hai giai đoạn: Chuẩn bị và Thực thi.

  • Chuẩn bị: Ở giai đoạn chuẩn bị, một mẫu câu lệnh SQL được tạo và gửi đến máy chủ cơ sở dữ liệu. Máy chủ phân tích mẫu câu lệnh, thực hiện kiểm tra cú pháp và tối ưu hóa truy vấn và lưu trữ nó để sử dụng sau. Một số các giá trị nhất định không được chỉ định mà thay bằng các ký tự ? (được gọi là tham số). Ví dụ: INSERT INTO Myguest VALUES (?,?,?).
  • Thực thi: Trong quá trình thực thi, các giá trị tham số được gửi đến máy chủ. Máy chủ tạo một câu lệnh từ mẫu câu lệnh và sử dụng các giá trị này để thực thi nó. Ứng dụng liên kết các giá trị với các tham số và CSDL thực thi câu lệnh. Ứng dụng có thể thực thi câu lệnh bao nhiêu lần tùy ý với các giá trị khác nhau.

CSDL sẽ phân tích cú pháp, biên dịch và thực hiện tối ưu hóa truy vấn trên mẫu câu lệnh SQL và lưu trữ kết quả biên dịch mà không thực thi nó.

3. Sử dụng Prepared Statement bằng MySQLi

3.1. Sử dụng MySQLi hướng thủ tục

Mysqli hướng thủ tục không hỗ trợ chúng ta prepared nhưng chúng ta hoàn toàn có thể dựa vào mysqli_stmt trung gian để prepare.

Ví dụ:

// khởi tạo kết nối
$connect = mysqli_connect('localhost', 'root', '', 'db_lttd');
//Kiểm tra kết nối
if (!$connect) {
    die('kết nối không thành công ' . mysqli_connect_error());
}
//câu truy vấn
$sql = "SELECT content FROM tbl_news WHERE id=? ";
//gán id
$id = 8;
//gán Mysqli sang mysqli_stmt
$stmt = mysqli_stmt_init($connect);
if (mysqli_stmt_prepare($stmt, $sql)) {
    //tiến hành truyền dữ liệu vào biến ẩn danh
    mysqli_stmt_bind_param($stmt, "i", $id);
    //chạy câu truy vấn
    mysqli_stmt_execute($stmt);
    // gán dữ liệu trả về vào biến $data
    mysqli_stmt_bind_result($stmt, $data);
    //thực hiện fetch dữ liệu
    mysqli_stmt_fetch($stmt);
    //in ra kết quả trả về
    printf('Result: %s', $data);
    //close mysqli_stmt
    mysqli_stmt_close($stmt);
}
//close mysqli
mysqli_close($connect);

3.2. Sử dụng MySQLi hướng đối tượng

Ví dụ sau sẽ sử dụng các câu lệnh được chuẩn bị và liên kết các tham số trong MySQLi:

connect_error) {
  die("Connection failed: " . $conn->connect_error);
}
//chuẩn bị và ràng buộc
$stmt = $conn->prepare("INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)");
$stmt->bind_param("sss", $firstname, $lastname, $email);
// thiết lập các tham số và thực thi
$firstname = "Thanh";
$lastname = "Nguyen";
$email = "";
$stmt->execute();

$firstname = "Nam";
$lastname = "Chu";
$email = "";
$stmt->execute();

$firstname = "Son";
$lastname = "Phung";
$email = "";
$stmt->execute();

echo "Các bản ghi mới đã được tạo thành công";

$stmt->close();
$conn->close();
?>

Như bạn đã thấy thì trong ví dụ trên có câu truy vấn SQL là:

“INSERT INTO MyGuests (firstname, lastname, email) VALUES (?, ?, ?)”

Trong truy vấn SQL này thì mình đã chèn một dấu ? đây chính là nơi sẽ thay thế bằng một giá trị kiểu integer, string, double hoặc BLOB.

Sau đó là việc gọi hàm bind_param() :

$stmt->bind_param("sss", $firstname, $lastname, $email);

Hàm này liên kết các tham số với truy vấn SQL và cho CSDL biết các tham số là gì. Đối số ‘sss’ liệt kê các kiểu dữ liệu của tham số. Ký tự s sẽ nói với mysql rằng tham số là một chuỗi. Trong truy vấn thì chúng ta có 3 đối số kiểu string là $firstname, $lastname, $email nên ta có 3 ký tự s (‘sss’).

Đối số có thể là một trong bốn loại sau:

  • i – số nguyên
  • d – gấp đôi
  • s – chuỗi
  • b – BLOB

Chúng ta sẽ phải có một trong số này cho mỗi tham số. Bằng cách nói cho mysql kiểu dữ liệu nào được yêu cầu, chúng ta đã giảm thiểu rủi ro của SQL Injection.

Lưu ý

Nếu chúng ta muốn chèn bất kỳ dữ liệu nào từ các nguồn bên ngoài (như thông tin người dùng nhập vào), điều rất quan trọng là dữ liệu đó phải được làm sạch và xác thực.

4. Sử dụng Prepared Statement bằng PDO

Với PDO thì cú pháp có hơi khác chút xíu đó là tham số ẩn không phải là dấu hỏi nữa mà là ở dạng :varname.

Ví dụ:

try {
    // Kết nối
    $conn = new PDO("mysql:host=localhost;dbname=db_lttd", 'root', 'vertrigo');
    // Khai baso exception
    $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    // prepare sql and bind parameters
    $stmt = $conn->prepare("INSERT INTO News (title, content) VALUES (:title, :content)");
    $stmt->bindParam(':title', $title);
    $stmt->bindParam(':content', $content);
 
    // Thêm lần 1
    $title = 'Tiêu đề 1';
    $content = 'Nội dung 1';
    $stmt->execute();
    // Thêm lần 2
    $title = 'Tiêu đề 2';
    $content = 'Nội dung 2';
    $stmt->execute();
    echo "Thao tác thành công!";
} 
catch (PDOException $e) {
    echo "Error: " . $e->getMessage();
}
// Ngắt kết nối
$conn = null;

Việc giải thích ví dụ này cũng tương tự như phần trên nhé!

5. Ưu điểm của việc sử dụng Prepared Statement

Prepared Statement có thể thực hiện cùng một câu lệnh lặp đi lặp lại với hiệu quả cao, bởi vì câu lệnh chỉ được phân tích cú pháp một lần, trong khi nó có thể được thực thi nhiều lần. Nó cũng giảm thiểu việc sử dụng băng thông, vì mỗi lần thực thi, chỉ có các giá trị giữ chỗ cần được truyền đến máy chủ cơ sở dữ liệu thay vì truyền cả câu lệnh SQL hoàn chỉnh.

Prepared Statement cũng cung cấp sự bảo vệ mạnh mẽ chống lại SQL injection (Hành động cố gắng truyền câu lệnh SQL để thao túng CSDL), bởi vì các giá trị tham số không được nhúng trực tiếp bên trong chuỗi truy vấn SQL. Các giá trị tham số được gửi đến máy chủ cơ sở dữ liệu tách biệt với truy vấn bằng một giao thức khác và do đó không thể can thiệp vào nó.

Máy chủ sử dụng các giá trị này trực tiếp tại điểm thực hiện, sau khi mẫu câu lệnh được phân tích cú pháp. Đó là lý do tại sao các Prepared Statement ít bị lỗi hơn và do đó được coi là một trong những yếu tố quan trọng nhất trong bảo mật cơ sở dữ liệu.