Bài 11. Xử lý form: GET, POST, xác thực và làm sạch dữ liệu

Form là cầu nối giữa người dùng và PHP. Bài này đi qua toàn bộ vòng đời xử lý form an toàn: nhận dữ liệu từ $_GET/$_POST, xác thực, làm sạch, hiển thị lỗi, giữ lại giá trị đã nhập, chống XSS/CSRF và mẫu PRG.

60 phútTrung cấp
Bạn sẽ học được
  • Đọc dữ liệu từ $_GET, $_POST, $_REQUEST
  • Xác thực (validate) và làm sạch (sanitize) dữ liệu
  • Hiển thị lỗi từng trường và giữ giá trị đã nhập
  • Áp dụng PRG, CSRF token, htmlspecialchars
Thuật ngữ cần biết
Validation
Kiểm tra dữ liệu đúng quy tắc: bắt buộc, định dạng, độ dài.
Sanitization
Làm sạch: trim, bỏ thẻ, chuẩn hóa.
PRG
Post → Redirect → Get: chuyển hướng sau khi xử lý POST.
CSRF
Giả mạo yêu cầu từ trang khác; chống bằng token.

Khái niệm và lý thuyết

Khi người dùng bấm gửi, trình duyệt gửi các cặp name=giá trị; PHP đưa vào $_GET (method get) hoặc $_POST (method post). Nguyên tắc số một: không bao giờ tin dữ liệu từ người dùng. Kiểm tra ở JavaScript chỉ để tiện; kiểm tra ở PHP là bắt buộc.

BướcViệc làmHàm
1. NhậnĐọc với giá trị mặc định$_POST["ten"] ?? ""
2. Làm sạchBỏ khoảng trắng, chuẩn hóatrim, mb_strtolower
3. Xác thựcBắt buộc, định dạng, miền giá trịfilter_var, preg_match, mb_strlen
4. Xử lýLưu CSDL, gửi mailprepared statement, password_hash
5. Phản hồiChuyển hướng, báo kết quảheader("Location: ..."), session
Hiển thịLuôn mã hóa đầu rahtmlspecialchars

GET: tìm kiếm

<form method="get" action="tim.php">
  <input name="q" value="<?= htmlspecialchars($_GET['q'] ?? '') ?>" placeholder="Tìm sinh viên">
  <button>Tìm</button>
</form>
<?php
$q = trim($_GET["q"] ?? "");
if ($q !== "") {
    echo "<p>Kết quả cho: <b>" . htmlspecialchars($q) . "</b></p>";
    // tìm trong CSDL (loạt PHP và MySQL)
}

URL trở thành tim.php?q=An: có thể lưu, chia sẻ. GET dùng cho tìm kiếm, lọc, phân trang.

POST: form đăng ký hoàn chỉnh

<?php
// dang-ky.php
session_start();
$loi = []; $du_lieu = ["ten" => "", "email" => "", "lop" => ""];

if ($_SERVER["REQUEST_METHOD"] === "POST") {
    // 1. Nhận + làm sạch
    foreach ($du_lieu as $k => $v) $du_lieu[$k] = trim($_POST[$k] ?? "");
    $mat_khau = $_POST["mat_khau"] ?? "";

    // 2. Xác thực
    if ($du_lieu["ten"] === "") $loi["ten"] = "Vui lòng nhập họ tên";
    elseif (mb_strlen($du_lieu["ten"]) < 3) $loi["ten"] = "Họ tên quá ngắn";
    if (!filter_var($du_lieu["email"], FILTER_VALIDATE_EMAIL)) $loi["email"] = "Email không hợp lệ";
    if (!in_array($du_lieu["lop"], ["K65 Tin", "K66 Tin"], true)) $loi["lop"] = "Lớp không hợp lệ";
    if (strlen($mat_khau) < 8) $loi["mat_khau"] = "Mật khẩu tối thiểu 8 ký tự";
    if (!hash_equals($_SESSION["csrf"] ?? "", $_POST["csrf"] ?? "")) $loi["csrf"] = "Phiên không hợp lệ";

    // 3. Xử lý + PRG
    if (!$loi) {
        $hash = password_hash($mat_khau, PASSWORD_DEFAULT);   // không lưu mật khẩu thô
        // lưu CSDL bằng prepared statement...
        $_SESSION["thong_bao"] = "Đăng ký thành công!";
        header("Location: dang-ky.php", true, 303);
        exit;
    }
}
$_SESSION["csrf"] = $_SESSION["csrf"] ?? bin2hex(random_bytes(16));
function e($s) { return htmlspecialchars($s, ENT_QUOTES, "UTF-8"); }
?>
<?php if (!empty($_SESSION["thong_bao"])): ?>
  <p style="color:green"><?= e($_SESSION["thong_bao"]) ?></p>
  <?php unset($_SESSION["thong_bao"]); ?>
<?php endif; ?>
<form method="post" novalidate>
  <input type="hidden" name="csrf" value="<?= e($_SESSION["csrf"]) ?>">
  <p><label>Họ tên <input name="ten" value="<?= e($du_lieu["ten"]) ?>"></label>
     <small style="color:red"><?= e($loi["ten"] ?? "") ?></small></p>
  <p><label>Email <input name="email" value="<?= e($du_lieu["email"]) ?>"></label>
     <small style="color:red"><?= e($loi["email"] ?? "") ?></small></p>
  <p><label>Lớp <select name="lop">
     <?php foreach (["K65 Tin", "K66 Tin"] as $l): ?>
       <option <?= $du_lieu["lop"] === $l ? "selected" : "" ?>><?= e($l) ?></option>
     <?php endforeach; ?></select></label></p>
  <p><label>Mật khẩu <input type="password" name="mat_khau"></label>
     <small style="color:red"><?= e($loi["mat_khau"] ?? "") ?></small></p>
  <button>Đăng ký</button>
</form>

Mẫu trên gộp hiển thị và xử lý trong một tệp (self-processing form): dễ giữ lại giá trị đã nhập và hiện lỗi ngay cạnh ô. Sau khi thành công, chuyển hướng 303 để tải lại trang không gửi lại form.

Checkbox, radio, select nhiều

<?php
$so_thich = $_POST["so_thich"] ?? [];        // name="so_thich[]" -> mảng
$so_thich = array_intersect($so_thich, ["doc", "the_thao", "nhac"]);  // chỉ giữ giá trị hợp lệ
$dong_y = isset($_POST["dong_y"]);            // checkbox không gửi khi không chọn
$he = $_POST["he"] ?? "";                     // radio
$tuoi = filter_input(INPUT_POST, "tuoi", FILTER_VALIDATE_INT, ["options" => ["min_range" => 16, "max_range" => 60]]);
if ($tuoi === false) $loi["tuoi"] = "Tuổi từ 16 đến 60";

Các hàm filter_var hữu ích

Bộ lọcMục đích
FILTER_VALIDATE_EMAILEmail hợp lệ
FILTER_VALIDATE_INT, _FLOATSố, có min/max
FILTER_VALIDATE_URLĐịa chỉ web
FILTER_VALIDATE_BOOLEANon/off, yes/no, 1/0
FILTER_SANITIZE_NUMBER_INTGiữ lại chữ số
FILTER_SANITIZE_SPECIAL_CHARSMã hóa ký tự đặc biệt
Mẹo hay

Đọc kỹ loạt bài HTML - Form (6 bài chi tiết) để nắm phía giao diện; bài này là phía máy chủ tương ứng.

Lưu ý

Không lưu mật khẩu dạng thô hay md5. Dùng password_hash khi lưu và password_verify khi đăng nhập.

Thực hành
  1. Hoàn thiện form đăng ký ở trên, thêm trường ngày sinh (kiểm tra ≥ 16 tuổi) và số điện thoại (preg_match).
  2. Viết form đăng nhập dùng password_verify với hash đã tạo sẵn.
  3. Làm form góp ý có textarea, giới hạn 500 ký tự, chống XSS khi hiển thị lại.
  4. Thử tấn công: nhập <script> vào ô tên và kiểm tra trang không bị chèn mã.
Tóm tắt bài học
  • $_GET cho tìm kiếm, $_POST cho dữ liệu nhạy cảm/thay đổi.
  • Nhận → làm sạch → xác thực → xử lý → chuyển hướng (PRG).
  • Hiện lỗi cạnh ô, giữ giá trị; htmlspecialchars mọi đầu ra.
  • CSRF token, password_hash, filter_var là tiêu chuẩn tối thiểu.