Prepared Statements (câu lệnh chuẩn bị) trong PHP và MySQL là một phương pháp giúp bảo vệ ứng dụng khỏi các cuộc tấn công SQL Injection và cải thiện hiệu suất khi thực hiện các truy vấn SQL lặp lại. Prepared Statements tách biệt phần câu lệnh SQL và dữ liệu đầu vào, giúp đảm bảo rằng các giá trị đầu vào được xử lý an toàn trước khi chèn vào truy vấn SQL.

Prepared Statements có thể được sử dụng với cả mysqliPDO trong PHP. Bài viết này sẽ hướng dẫn cách sử dụng Prepared Statements với cả hai phương pháp.

1. Prepared Statements là gì?

Prepared Statements là một cơ chế mà câu lệnh SQL được phân tách thành hai phần:

  1. Chuẩn bị câu lệnh: Câu lệnh SQL được gửi đến cơ sở dữ liệu, không chứa các giá trị đầu vào (dữ liệu người dùng).
  2. Thực thi câu lệnh: Các giá trị đầu vào được liên kết với các tham số placeholder và câu lệnh được thực thi. Cơ sở dữ liệu sẽ tự động thoát (escape) các giá trị đầu vào, bảo vệ khỏi SQL Injection.

Lợi ích của Prepared Statements:

  • Bảo mật: Giảm thiểu nguy cơ tấn công SQL Injection vì các giá trị đầu vào được xử lý riêng biệt khỏi câu lệnh SQL.
  • Hiệu suất: Khi cần thực hiện nhiều lần câu lệnh SQL cùng một cấu trúc với các giá trị khác nhau, Prepared Statements cho phép tái sử dụng câu lệnh đã chuẩn bị trước đó.

2. Sử dụng Prepared Statements với mysqli

a) Prepared Statements - Hướng đối tượng (OOP)

php
<?php
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "my_database";

// Tạo kết nối
$conn = new mysqli($servername, $username, $password, $dbname);

// Kiểm tra kết nối
if ($conn->connect_error) {
    die("Kết nối thất bại: " . $conn->connect_error);
}

// Chuẩn bị câu lệnh SQL
$stmt = $conn->prepare("INSERT INTO Users (username, email) VALUES (?, ?)");

// Liên kết các tham số
$stmt->bind_param("ss", $username, $email);

// Gán giá trị cho các biến và thực thi câu lệnh nhiều lần
$username = "john_doe";
$email = "john@example.com";
$stmt->execute();

$username = "jane_doe";
$email = "jane@example.com";
$stmt->execute();

echo "Bản ghi đã được chèn thành công";

// Đóng câu lệnh và kết nối
$stmt->close();
$conn->close();
?>

Giải thích:

  • prepare(): Chuẩn bị câu lệnh SQL với các tham số placeholder (?).
  • bind_param(): Liên kết các biến với các tham số. Trong ví dụ này, ss đại diện cho hai biến chuỗi (s là kiểu string).
  • execute(): Thực thi câu lệnh với các giá trị hiện tại của các biến được liên kết.

b) Prepared Statements - Hướng thủ tục

php
<?php
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "my_database";

// Tạo kết nối
$conn = mysqli_connect($servername, $username, $password, $dbname);

// Kiểm tra kết nối
if (!$conn) {
    die("Kết nối thất bại: " . mysqli_connect_error());
}

// Chuẩn bị câu lệnh SQL
$stmt = mysqli_prepare($conn, "INSERT INTO Users (username, email) VALUES (?, ?)");

// Liên kết các tham số
mysqli_stmt_bind_param($stmt, "ss", $username, $email);

// Gán giá trị cho các biến và thực thi câu lệnh nhiều lần
$username = "alice";
$email = "alice@example.com";
mysqli_stmt_execute($stmt);

$username = "bob";
$email = "bob@example.com";
mysqli_stmt_execute($stmt);

echo "Bản ghi đã được chèn thành công";

// Đóng câu lệnh và kết nối
mysqli_stmt_close($stmt);
mysqli_close($conn);
?>

3. Sử dụng Prepared Statements với PDO

Prepared Statements trong PDO rất mạnh mẽ và đơn giản, đồng thời hỗ trợ nhiều kiểu cơ sở dữ liệu khác ngoài MySQL.

Ví dụ với PDO:

php
<?php
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "my_database";

try {
    // Tạo kết nối PDO
    $conn = new PDO("mysql:host=$servername;dbname=$dbname", $username, $password);
    $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // Chuẩn bị câu lệnh SQL
    $stmt = $conn->prepare("INSERT INTO Users (username, email) VALUES (:username, :email)");

    // Liên kết các tham số
    $stmt->bindParam(':username', $username);
    $stmt->bindParam(':email', $email);

    // Gán giá trị cho các biến và thực thi câu lệnh nhiều lần
    $username = "charlie";
    $email = "charlie@example.com";
    $stmt->execute();

    $username = "dave";
    $email = "dave@example.com";
    $stmt->execute();

    echo "Bản ghi đã được chèn thành công";
} catch(PDOException $e) {
    echo "Lỗi khi chèn bản ghi: " . $e->getMessage();
}

// Đóng kết nối
$conn = null;
?>

Giải thích:

  • prepare(): Chuẩn bị câu lệnh SQL với các tham số placeholder được đánh dấu bằng :param.
  • bindParam(): Liên kết các biến với các tham số SQL. Bạn có thể sử dụng bindValue() nếu bạn không muốn giá trị thay đổi sau khi liên kết.
  • execute(): Thực thi câu lệnh đã chuẩn bị với các giá trị hiện tại của các tham số.

4. Lấy dữ liệu với Prepared Statements

Prepared Statements cũng có thể được sử dụng để thực hiện các truy vấn SELECT an toàn.

a) Lấy dữ liệu với mysqli

php
<?php
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "my_database";

// Tạo kết nối
$conn = new mysqli($servername, $username, $password, $dbname);

// Kiểm tra kết nối
if ($conn->connect_error) {
    die("Kết nối thất bại: " . $conn->connect_error);
}

// Chuẩn bị câu lệnh SQL
$stmt = $conn->prepare("SELECT id, username, email FROM Users WHERE username = ?");
$stmt->bind_param("s", $username);

// Gán giá trị cho biến và thực thi câu lệnh
$username = "john_doe";
$stmt->execute();

// Liên kết kết quả với các biến
$stmt->bind_result($id, $username, $email);

// Lặp qua các bản ghi kết quả
while ($stmt->fetch()) {
    echo "ID: $id - Username: $username - Email: $email\n";
}

// Đóng câu lệnh và kết nối
$stmt->close();
$conn->close();
?>

b) Lấy dữ liệu với PDO

php
<?php
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "my_database";

try {
    // Tạo kết nối PDO
    $conn = new PDO("mysql:host=$servername;dbname=$dbname", $username, $password);
    $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // Chuẩn bị câu lệnh SQL
    $stmt = $conn->prepare("SELECT id, username, email FROM Users WHERE username = :username");

    // Liên kết tham số
    $stmt->bindParam(':username', $username);

    // Gán giá trị và thực thi câu lệnh
    $username = "john_doe";
    $stmt->execute();

    // Lấy dữ liệu trả về dưới dạng mảng kết hợp
    $result = $stmt->fetchAll(PDO::FETCH_ASSOC);

    foreach ($result as $row) {
        echo "ID: " . $row['id'] . " - Username: " . $row['username'] . " - Email: " . $row['email'] . "\n";
    }
} catch(PDOException $e) {
    echo "Lỗi: " . $e->getMessage();
}

// Đóng kết nối
$conn = null;
?>

5. Khi nào nên sử dụng Prepared Statements?

  • SQL Injection: Khi làm việc với dữ liệu người dùng (từ form, URL, hoặc API), Prepared Statements là một công cụ quan trọng để bảo vệ cơ sở dữ liệu khỏi các cuộc tấn công SQL Injection.
  • Thực hiện truy vấn lặp lại: Khi bạn cần thực hiện một truy vấn nhiều lần với các dữ liệu khác nhau, Prepared Statements giúp cải thiện hiệu suất vì câu lệnh SQL chỉ cần chuẩn bị một lần.

6. Kết luận

Prepared Statements là một công cụ mạnh mẽ giúp tăng tính bảo mật và hiệu suất khi làm việc với cơ sở dữ liệu MySQL trong PHP. Bằng cách tách biệt câu lệnh SQL và dữ liệu người dùng, bạn có thể ngăn chặn các cuộc tấn công SQL Injection và bảo vệ cơ sở dữ liệu của mình.

  • mysqli: Hỗ trợ cả hai kiểu hướng đối tượng và hướng thủ tục.
  • PDO: Cung cấp một API nhất quán và linh hoạt cho nhiều loại cơ sở dữ liệu, không chỉ MySQL.

Sử dụng Prepared Statements là một cách tốt để bảo đảm ứng dụng PHP của bạn an toàn và hiệu quả.