PHP và MySQL Bài 17: Mã hóa mật khẩu trong PHP - Chuẩn mực bảo mật với password_hash()
Khi xây dựng hệ thống (đặc biệt là các hệ thống liên quan đến tài chính, vận hành, kiểm soát vé), bạn phải luôn chuẩn bị cho tình huống xấu nhất: Cơ sở dữ liệu (Database) bị hacker đánh cắp.
Nếu mật khẩu của người dùng được lưu dưới dạng văn bản thuần túy (Plaintext) như 123456 hay admin@2026, hacker sẽ lập tức chiếm quyền toàn bộ hệ thống. Nguyên tắc bất di bất dịch của Backend Developer là: Không bao giờ lưu mật khẩu gốc vào Database. Phải "băm" (Hash) nó ra.
1. Tại sao MD5 và SHA1 đã hoàn toàn "hết thời"?
Trước đây, lập trình viên thường dùng hàm md5() hoặc sha1() để băm mật khẩu. Quá trình băm (Hashing) là mã hóa một chiều: bạn có thể biến 123456 thành một chuỗi loằng ngoằng, nhưng không thể dịch ngược chuỗi đó lại thành 123456.
Tuy nhiên, MD5 và SHA1 hiện nay bị coi là cực kỳ nguy hiểm vì 2 lý do:
- Tốc độ quá nhanh: Các card đồ họa hiện đại có thể tính toán hàng tỷ mã MD5 mỗi giây. Hacker có thể dùng phương pháp Brute-force (thử sai liên tục) để đoán ngược ra mật khẩu gốc trong chớp mắt.
- Bảng tra cứu ngược (Rainbow Tables): Hacker đã tính toán sẵn hàng tỷ mật khẩu thông dụng và lưu mã MD5 của chúng thành một từ điển. Chỉ cần đối chiếu chuỗi mã hóa bị lộ với từ điển này là ra ngay mật khẩu gốc.
2. Kỷ nguyên của Bcrypt và hàm password_hash()
Từ phiên bản 5.5, PHP đã tích hợp sẵn một bộ công cụ mã hóa mật khẩu cực kỳ an toàn, sử dụng thuật toán Bcrypt (hoặc Argon2). Thuật toán này cố tình chạy chậm và tự động thêm một chuỗi ngẫu nhiên (gọi là Salt) vào mật khẩu trước khi băm.
Nhờ có Salt ngẫu nhiên, dù hai người dùng đặt chung một mật khẩu là 123456, thì chuỗi mã hóa lưu trong Database của họ vẫn hoàn toàn khác nhau. Điều này vô hiệu hóa hoàn toàn các bảng tra cứu Rainbow Tables.
Cú pháp tạo mã băm:
<?php
$matKhauGoc = "SecureAdmin_ST05!";
// Sử dụng thuật toán mặc định và an toàn nhất hiện tại (Bcrypt)
$matKhauDaBam = password_hash($matKhauGoc, PASSWORD_DEFAULT);
echo "Mật khẩu gốc: " . $matKhauGoc . "\n";
echo "Chuỗi lưu vào Database: " . $matKhauDaBam;
// Kết quả sẽ là một chuỗi dài 60 ký tự, ví dụ: $2y$10$Y1abc...
?>
3. Cách xác thực mật khẩu với password_verify()
Vì mỗi lần chạy hàm password_hash(), PHP lại sinh ra một chuỗi khác nhau (do Salt ngẫu nhiên), nên bạn không thể dùng toán tử === để so sánh mật khẩu người dùng nhập vào với chuỗi mã hóa trong CSDL.
Thay vào đó, PHP cung cấp hàm password_verify(). Hàm này sẽ tự động phân tích Salt ẩn bên trong chuỗi mã hóa và thực hiện đối chiếu một cách an toàn.
<?php
$nhapTuForm = "SecureAdmin_ST05!"; // Mật khẩu người dùng nhập lúc Đăng nhập
$hashTuDatabase = '$2y$10$K7Z3...'; // Chuỗi mã hóa 60 ký tự lấy ra từ Database
// Trả về true nếu khớp, false nếu sai
if (password_verify($nhapTuForm, $hashTuDatabase)) {
echo "Mật khẩu chính xác. Cấp quyền truy cập hệ thống!";
// Tiến hành khởi tạo Session ở đây (Như Bài 16)
} else {
echo "Lỗi: Mật khẩu không đúng.";
}
?>
4. Kịch bản Đăng ký và Đăng nhập thực tế
Dưới đây là mô phỏng luồng dữ liệu chuẩn cho hai tính năng quan trọng nhất của mọi ứng dụng web:
Luồng 1: Xử lý Đăng ký (Lưu dữ liệu mới)
Khi người dùng điền Form đăng ký, hệ thống sẽ tiếp nhận mật khẩu, băm nó ra rồi mới đưa lệnh lưu vào Database.
<?php
// Dữ liệu từ Form Đăng ký
$username = $_POST['username'];
$rawPassword = $_POST['password'];
// 1. Validate độ mạnh của mật khẩu (vd: ít nhất 8 ký tự)
if (strlen($rawPassword) < 8) {
die("Mật khẩu quá yếu, phải từ 8 ký tự trở lên.");
}
// 2. Băm mật khẩu
$hashedPassword = password_hash($rawPassword, PASSWORD_DEFAULT);
// 3. (Giả lập) Lưu vào Database
// SQL: INSERT INTO users (username, password) VALUES ('$username', '$hashedPassword')
echo "Tạo tài khoản thành công!";
?>
Luồng 2: Xử lý Đăng nhập (Xác thực dữ liệu)
Khi người dùng muốn đăng nhập, hệ thống lấy chuỗi băm từ Database ra để đối chiếu với mật khẩu họ vừa nhập.
<?php
// Dữ liệu từ Form Đăng nhập
$inputUsername = $_POST['username'];
$inputPassword = $_POST['password'];
// 1. (Giả lập) Truy vấn Database để tìm người dùng này
// SQL: SELECT password FROM users WHERE username = '$inputUsername'
// Giả sử hệ thống trả về biến $dbHash
$dbHash = '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm'; // Ví dụ mã băm của 'secret123'
// 2. So sánh mật khẩu bằng password_verify
if ($dbHash && password_verify($inputPassword, $dbHash)) {
session_start();
$_SESSION['user'] = $inputUsername;
echo "Đăng nhập thành công!";
} else {
echo "Tài khoản hoặc mật khẩu không chính xác.";
}
?>
Tổng kết Bài 17 và Khép lại Phần 4
Tuyệt đối không tự viết thuật toán mã hóa mật khẩu, và hãy để password_hash() cùng password_verify() làm nhiệm vụ của chúng. Đây là tiêu chuẩn vàng hiện nay.
Chúng ta đã chính thức khép lại Phần 4. Tới đây, bạn đã biết cách dựng giao diện, nhận dữ liệu, xử lý logic, quản lý phiên làm việc và mã hóa thông tin an toàn. Nhưng mọi hệ thống đều vô nghĩa nếu không có một "kho chứa" lâu dài.
Bài tiếp theo sẽ là một bước ngoặt lớn, mở màn cho Phần 5: Cơ Sở Dữ Liệu MySQL - Từ Gốc Đến Ngọn. Chúng ta sẽ bắt đầu với Bài 18: Giới thiệu MySQL, hệ quản trị cơ sở dữ liệu và các kiểu dữ liệu trong MySQL, nơi lưu trữ linh hồn của toàn bộ ứng dụng.
All Rights Reserved