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

Các câu lệnh được chuẩn bị [prepared statements] rất hữu ích để chống lại các cuộc tấn công SQL Injection.

Chuẩn bị câu lệnh và liên kết tham số

Một câu lệnh được chuẩn bị [prepared statement] là một tính năng được sử dụng để thực thi các câu lệnh SQL giống nhau [hoặc tương tự] lặp đi lặp lại với hiệu quả cao.

Một câu lệnh được chuẩn bị về cơ bản hoạt động như thế này:

  1. Chuẩn bị: Một mẫu câu lệnh SQL được tạo và gửi đến cơ sở dữ liệu. Một số 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 MyGuests VALUES[?, ?, ?]
  2. Cơ sở dữ liệu 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. Thực thi: Sau đó, ứng dụng liên kết các giá trị với các tham số và cơ sở dữ liệu 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

So với việc thực thi trực tiếp các câu lệnh SQL, các câu lệnh được chuẩn bị có ba ưu điểm chính:

  • Các câu lệnh được chuẩn bị giúp giảm thời gian phân tích cú pháp vì việc chuẩn bị truy vấn chỉ được thực hiện một lần [mặc dù câu lệnh được thực thi nhiều lần]
  • Liên kết các tham số giúp thu nhỏ băng thông đến máy chủ vì bạn chỉ cần gửi các tham số mỗi lần truy vấn thay vì phải gửi toàn bộ truy vấn.
  • Các câu lệnh được chuẩn bị rất hữu ích để chống lại SQL Injection. Bởi vì các giá trị tham số được truyền vào sau bằng một giao thức khác, không cần phải lọc các ký tự đặc biệt. Nếu mẫu câu lệnh gốc không xuất phát từ dữ liệu đầu vào từ bên ngoài, SQL Injection không thể xảy ra.

Chuẩn bị câu lệnh trong MySQLi

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

MySQLi hướng đối tượng

Như bạn thấy trong ví dụ trên có câu truy vấn SQL như sau:


"INSERT INTO MyGuests [firstname, lastname, email] VALUES [?, ?, ?]"

Trong truy vấn SQL này chúng tôi chèn một dấu hỏi [?], đây là nơi chúng tôi muốn thay thế bằng một giá trị kiểu integer, string, double hoặc BLOB.

Sau đó, chúng tôi gọi hàm bind_param[] như sau:


$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 cơ sở dữ liệu biết các tham số là gì. Đối số "sss" liệt kê các kiểu dữ liệu của các tham số. Ký tự s nói với mysql rằng tham số là một chuỗi. Trong truy vấn chúng ta có 3 đối số kiểu string là $firstname, $lastname, $email nên chúng ta sẽ có 3 ký tự s ["sss"].

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

  • i - integer
  • d - double
  • s - string
  • b - BLOB

Chúng ta 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.

Cơ sở dữ liệu MySQL hỗ trợ các câu lệnh đã chuẩn bị. Một tuyên bố đã chuẩn bị hoặc một câu lệnh tham số hóa được sử dụng để thực hiện cùng một tuyên bố nhiều lần với hiệu quả cao và bảo vệ chống tiêm SQL.

Quy trình công việc cơ bản

Việc thực hiện tuyên bố đã chuẩn bị bao gồm hai giai đoạn: chuẩn bị và thực thi. Ở giai đoạn chuẩn bị, một mẫu câu lệnh được gửi đến máy chủ cơ sở dữ liệu. Máy chủ thực hiện kiểm tra cú pháp và khởi tạo tài nguyên nội bộ của máy chủ để sử dụng sau.

Máy chủ MySQL hỗ trợ bằng cách sử dụng trình giữ chỗ ẩn danh, định vị với

array[3] {
  [0]=>
  array[2] {
    ["id"]=>
    string[1] "1"
    ["label"]=>
    string[3] "PHP"
  }
  [1]=>
  array[2] {
    ["id"]=>
    string[1] "2"
    ["label"]=>
    string[4] "Java"
  }
  [2]=>
  array[2] {
    ["id"]=>
    string[1] "3"
    ["label"]=>
    string[3] "C++"
  }
}
7.

Chuẩn bị được theo sau bởi thực thi. Trong quá trình thực hiện, máy khách liên kết các giá trị tham số và gửi chúng đến máy chủ. Máy chủ thực thi câu lệnh với các giá trị bị ràng buộc bằng các tài nguyên nội bộ được tạo trước đó.

Ví dụ số 1 tuyên bố đã chuẩn bị

array[3] {
  [0]=>
  array[2] {
    ["id"]=>
    string[1] "1"
    ["label"]=>
    string[3] "PHP"
  }
  [1]=>
  array[2] {
    ["id"]=>
    string[1] "2"
    ["label"]=>
    string[4] "Java"
  }
  [2]=>
  array[2] {
    ["id"]=>
    string[1] "3"
    ["label"]=>
    string[3] "C++"
  }
}
8

array[3] {
  [0]=>
  array[2] {
    ["id"]=>
    string[1] "1"
    ["label"]=>
    string[3] "PHP"
  }
  [1]=>
  array[2] {
    ["id"]=>
    string[1] "2"
    ["label"]=>
    string[4] "Java"
  }
  [2]=>
  array[2] {
    ["id"]=>
    string[1] "3"
    ["label"]=>
    string[3] "C++"
  }
}
9

id = 1 [integer]
label = PHP [string]
0

Thực hiện lặp đi lặp lại

Một tuyên bố đã chuẩn bị có thể được thực hiện nhiều lần. Khi mỗi lần thực hiện, giá trị hiện tại của biến ràng buộc được đánh giá và gửi đến máy chủ. Tuyên bố không được phân tích cú pháp một lần nữa. Mẫu câu lệnh không được chuyển đến máy chủ một lần nữa.

Ví dụ #2 Chèn chuẩn bị một lần, được thực hiện nhiều lần

array[3] {
  [0]=>
  array[2] {
    ["id"]=>
    string[1] "1"
    ["label"]=>
    string[3] "PHP"
  }
  [1]=>
  array[2] {
    ["id"]=>
    string[1] "2"
    ["label"]=>
    string[4] "Java"
  }
  [2]=>
  array[2] {
    ["id"]=>
    string[1] "3"
    ["label"]=>
    string[3] "C++"
  }
}
8

array[3] {
  [0]=>
  array[2] {
    ["id"]=>
    string[1] "1"
    ["label"]=>
    string[3] "PHP"
  }
  [1]=>
  array[2] {
    ["id"]=>
    string[1] "2"
    ["label"]=>
    string[4] "Java"
  }
  [2]=>
  array[2] {
    ["id"]=>
    string[1] "3"
    ["label"]=>
    string[3] "C++"
  }
}
9

id = 1 [integer]
label = PHP [string]
1

Ví dụ trên sẽ xuất ra:

array[3] {
  [0]=>
  array[2] {
    ["id"]=>
    string[1] "1"
    ["label"]=>
    string[3] "PHP"
  }
  [1]=>
  array[2] {
    ["id"]=>
    string[1] "2"
    ["label"]=>
    string[4] "Java"
  }
  [2]=>
  array[2] {
    ["id"]=>
    string[1] "3"
    ["label"]=>
    string[3] "C++"
  }
}

Mỗi tuyên bố đã chuẩn bị chiếm tài nguyên máy chủ. Các tuyên bố nên được đóng một cách rõ ràng ngay sau khi sử dụng. Nếu không được thực hiện rõ ràng, câu lệnh sẽ được đóng khi xử lý câu lệnh được giải phóng bởi PHP.

Sử dụng một câu lệnh đã chuẩn bị không phải lúc nào cũng là cách hiệu quả nhất để thực hiện một tuyên bố. Một câu lệnh được chuẩn bị chỉ được thực hiện một lần gây ra nhiều chuyến đi vòng khách máy khách hơn so với một câu lệnh không được chuẩn bị. Đây là lý do tại sao

id = 1 [integer]
label = PHP [string]
2 không được chạy như một tuyên bố đã chuẩn bị ở trên.

Ngoài ra, hãy xem xét việc sử dụng cú pháp SQL đa điểm của MySQL để chèn. Ví dụ, Multi-Insert yêu cầu ít chuyến đi vòng nào giữa máy chủ và máy khách so với câu lệnh đã chuẩn bị ở trên.

Ví dụ #3 Các chuyến đi vòng ít sử dụng SQL đa điểm

array[3] {
  [0]=>
  array[2] {
    ["id"]=>
    string[1] "1"
    ["label"]=>
    string[3] "PHP"
  }
  [1]=>
  array[2] {
    ["id"]=>
    string[1] "2"
    ["label"]=>
    string[4] "Java"
  }
  [2]=>
  array[2] {
    ["id"]=>
    string[1] "3"
    ["label"]=>
    string[3] "C++"
  }
}
8

array[3] {
  [0]=>
  array[2] {
    ["id"]=>
    string[1] "1"
    ["label"]=>
    string[3] "PHP"
  }
  [1]=>
  array[2] {
    ["id"]=>
    string[1] "2"
    ["label"]=>
    string[4] "Java"
  }
  [2]=>
  array[2] {
    ["id"]=>
    string[1] "3"
    ["label"]=>
    string[3] "C++"
  }
}
9

id = 1 [integer]
label = PHP [string]
5

Kết quả đặt các loại dữ liệu giá trị

Giao thức máy khách MySQL định nghĩa một giao thức truyền dữ liệu khác nhau cho các câu lệnh đã chuẩn bị và các câu lệnh không được chuẩn bị. Các câu lệnh được chuẩn bị đang sử dụng cái gọi là giao thức nhị phân. Máy chủ MySQL gửi dữ liệu đặt kết quả "như là" ở định dạng nhị phân. Kết quả không được tuần tự hóa thành chuỗi trước khi gửi. Thư viện máy khách nhận được dữ liệu nhị phân và cố gắng chuyển đổi các giá trị thành các loại dữ liệu PHP phù hợp. Ví dụ: kết quả từ cột SQL

id = 1 [integer]
label = PHP [string]
6 sẽ được cung cấp dưới dạng các biến số nguyên PHP.

Ví dụ #4 Kiểu dữ liệu gốc

array[3] {
  [0]=>
  array[2] {
    ["id"]=>
    string[1] "1"
    ["label"]=>
    string[3] "PHP"
  }
  [1]=>
  array[2] {
    ["id"]=>
    string[1] "2"
    ["label"]=>
    string[4] "Java"
  }
  [2]=>
  array[2] {
    ["id"]=>
    string[1] "3"
    ["label"]=>
    string[3] "C++"
  }
}
8

array[3] {
  [0]=>
  array[2] {
    ["id"]=>
    string[1] "1"
    ["label"]=>
    string[3] "PHP"
  }
  [1]=>
  array[2] {
    ["id"]=>
    string[1] "2"
    ["label"]=>
    string[4] "Java"
  }
  [2]=>
  array[2] {
    ["id"]=>
    string[1] "3"
    ["label"]=>
    string[3] "C++"
  }
}
9

id = 1 [integer]
label = PHP [string]
9

Ví dụ trên sẽ xuất ra:

id = 1 [integer]
label = PHP [string]

Mỗi tuyên bố đã chuẩn bị chiếm tài nguyên máy chủ. Các tuyên bố nên được đóng một cách rõ ràng ngay sau khi sử dụng. Nếu không được thực hiện rõ ràng, câu lệnh sẽ được đóng khi xử lý câu lệnh được giải phóng bởi PHP.

Sử dụng một câu lệnh đã chuẩn bị không phải lúc nào cũng là cách hiệu quả nhất để thực hiện một tuyên bố. Một câu lệnh được chuẩn bị chỉ được thực hiện một lần gây ra nhiều chuyến đi vòng khách máy khách hơn so với một câu lệnh không được chuẩn bị. Đây là lý do tại sao

id = 1 [integer]
label = PHP [string]
2 không được chạy như một tuyên bố đã chuẩn bị ở trên.

Ngoài ra, hãy xem xét việc sử dụng cú pháp SQL đa điểm của MySQL để chèn. Ví dụ, Multi-Insert yêu cầu ít chuyến đi vòng nào giữa máy chủ và máy khách so với câu lệnh đã chuẩn bị ở trên.mysqli_result object.

Ví dụ #3 Các chuyến đi vòng ít sử dụng SQL đa điểm

Kết quả đặt các loại dữ liệu giá trị

array[3] {
  [0]=>
  array[2] {
    ["id"]=>
    string[1] "1"
    ["label"]=>
    string[3] "PHP"
  }
  [1]=>
  array[2] {
    ["id"]=>
    string[1] "2"
    ["label"]=>
    string[4] "Java"
  }
  [2]=>
  array[2] {
    ["id"]=>
    string[1] "3"
    ["label"]=>
    string[3] "C++"
  }
}
8

array[3] {
  [0]=>
  array[2] {
    ["id"]=>
    string[1] "1"
    ["label"]=>
    string[3] "PHP"
  }
  [1]=>
  array[2] {
    ["id"]=>
    string[1] "2"
    ["label"]=>
    string[4] "Java"
  }
  [2]=>
  array[2] {
    ["id"]=>
    string[1] "3"
    ["label"]=>
    string[3] "C++"
  }
}
9

id = 1 [integer], label = PHP [string]
2

Ví dụ trên sẽ xuất ra:

id = 1 [integer], label = PHP [string]

Mỗi tuyên bố đã chuẩn bị chiếm tài nguyên máy chủ. Các tuyên bố nên được đóng một cách rõ ràng ngay sau khi sử dụng. Nếu không được thực hiện rõ ràng, câu lệnh sẽ được đóng khi xử lý câu lệnh được giải phóng bởi PHP.

Sử dụng một câu lệnh đã chuẩn bị không phải lúc nào cũng là cách hiệu quả nhất để thực hiện một tuyên bố. Một câu lệnh được chuẩn bị chỉ được thực hiện một lần gây ra nhiều chuyến đi vòng khách máy khách hơn so với một câu lệnh không được chuẩn bị. Đây là lý do tại sao

id = 1 [integer]
label = PHP [string]
2 không được chạy như một tuyên bố đã chuẩn bị ở trên.mysqli_stmt::store_result[].

Ngoài ra, hãy xem xét việc sử dụng cú pháp SQL đa điểm của MySQL để chèn. Ví dụ, Multi-Insert yêu cầu ít chuyến đi vòng nào giữa máy chủ và máy khách so với câu lệnh đã chuẩn bị ở trên.

Ví dụ #3 Các chuyến đi vòng ít sử dụng SQL đa điểmmysqli_stmt::get_result[] returns a buffered result set.

Kết quả đặt các loại dữ liệu giá trị

array[3] {
  [0]=>
  array[2] {
    ["id"]=>
    string[1] "1"
    ["label"]=>
    string[3] "PHP"
  }
  [1]=>
  array[2] {
    ["id"]=>
    string[1] "2"
    ["label"]=>
    string[4] "Java"
  }
  [2]=>
  array[2] {
    ["id"]=>
    string[1] "3"
    ["label"]=>
    string[3] "C++"
  }
}
8

array[3] {
  [0]=>
  array[2] {
    ["id"]=>
    string[1] "1"
    ["label"]=>
    string[3] "PHP"
  }
  [1]=>
  array[2] {
    ["id"]=>
    string[1] "2"
    ["label"]=>
    string[4] "Java"
  }
  [2]=>
  array[2] {
    ["id"]=>
    string[1] "3"
    ["label"]=>
    string[3] "C++"
  }
}
9

id = 1 [integer], label = PHP [string]
6

Ví dụ trên sẽ xuất ra:

array[3] {
  [0]=>
  array[2] {
    ["id"]=>
    string[1] "1"
    ["label"]=>
    string[3] "PHP"
  }
  [1]=>
  array[2] {
    ["id"]=>
    string[1] "2"
    ["label"]=>
    string[4] "Java"
  }
  [2]=>
  array[2] {
    ["id"]=>
    string[1] "3"
    ["label"]=>
    string[3] "C++"
  }
}
2

Mỗi tuyên bố đã chuẩn bị chiếm tài nguyên máy chủ. Các tuyên bố nên được đóng một cách rõ ràng ngay sau khi sử dụng. Nếu không được thực hiện rõ ràng, câu lệnh sẽ được đóng khi xử lý câu lệnh được giải phóng bởi PHP.mysqli_result interface offers the additional benefit of flexible client-side result set navigation.

Sử dụng một câu lệnh đã chuẩn bị không phải lúc nào cũng là cách hiệu quả nhất để thực hiện một tuyên bố. Một câu lệnh được chuẩn bị chỉ được thực hiện một lần gây ra nhiều chuyến đi vòng khách máy khách hơn so với một câu lệnh không được chuẩn bị. Đây là lý do tại sao

array[3] {
  [0]=>
  array[2] {
    ["id"]=>
    string[1] "1"
    ["label"]=>
    string[3] "PHP"
  }
  [1]=>
  array[2] {
    ["id"]=>
    string[1] "2"
    ["label"]=>
    string[4] "Java"
  }
  [2]=>
  array[2] {
    ["id"]=>
    string[1] "3"
    ["label"]=>
    string[3] "C++"
  }
}
32 không được chạy như một tuyên bố đã chuẩn bị ở trên.

array[3] {
  [0]=>
  array[2] {
    ["id"]=>
    string[1] "1"
    ["label"]=>
    string[3] "PHP"
  }
  [1]=>
  array[2] {
    ["id"]=>
    string[1] "2"
    ["label"]=>
    string[4] "Java"
  }
  [2]=>
  array[2] {
    ["id"]=>
    string[1] "3"
    ["label"]=>
    string[3] "C++"
  }
}
8

array[3] {
  [0]=>
  array[2] {
    ["id"]=>
    string[1] "1"
    ["label"]=>
    string[3] "PHP"
  }
  [1]=>
  array[2] {
    ["id"]=>
    string[1] "2"
    ["label"]=>
    string[4] "Java"
  }
  [2]=>
  array[2] {
    ["id"]=>
    string[1] "3"
    ["label"]=>
    string[3] "C++"
  }
}
9

id = 1 [integer], label = PHP [string]
9

Ví dụ trên sẽ xuất ra:

array[3] {
  [0]=>
  array[2] {
    ["id"]=>
    string[1] "1"
    ["label"]=>
    string[3] "PHP"
  }
  [1]=>
  array[2] {
    ["id"]=>
    string[1] "2"
    ["label"]=>
    string[4] "Java"
  }
  [2]=>
  array[2] {
    ["id"]=>
    string[1] "3"
    ["label"]=>
    string[3] "C++"
  }
}
5

Mỗi tuyên bố đã chuẩn bị chiếm tài nguyên máy chủ. Các tuyên bố nên được đóng một cách rõ ràng ngay sau khi sử dụng. Nếu không được thực hiện rõ ràng, câu lệnh sẽ được đóng khi xử lý câu lệnh được giải phóng bởi PHP.

Sử dụng một câu lệnh đã chuẩn bị không phải lúc nào cũng là cách hiệu quả nhất để thực hiện một tuyên bố. Một câu lệnh được chuẩn bị chỉ được thực hiện một lần gây ra nhiều chuyến đi vòng khách máy khách hơn so với một câu lệnh không được chuẩn bị. Đây là lý do tại sao

id = 1 [integer]
label = PHP [string]
2 không được chạy như một tuyên bố đã chuẩn bị ở trên.mysqli_stmt::bind_param[] function for more information.

Một sự tách biệt như vậy đôi khi được coi là tính năng bảo mật duy nhất để ngăn chặn tiêm SQL, nhưng cùng một mức độ bảo mật có thể đạt được với các câu lệnh không được chuẩn bị, nếu tất cả các giá trị được định dạng chính xác. Cần lưu ý rằng định dạng chính xác không giống như thoát ra và liên quan đến logic nhiều hơn so với việc trốn thoát đơn giản. Do đó, các câu lệnh được chuẩn bị chỉ đơn giản là một cách tiếp cận thuận tiện hơn và dễ bị lỗi hơn đối với yếu tố bảo mật cơ sở dữ liệu này.

Chủ Đề