[Series] Xây dựng RESTful API từ con số 0 với PHP Thuần & MVC - Phần 7: Cơ chế Đổi mật khẩu an toàn
Chào các bạn, mình đã quay trở lại!
Một trong những quy tắc vàng của bảo mật là khuyến khích người dùng đổi mật khẩu định kỳ. Tuy nhiên, lập trình viên thường mắc sai lầm khi cho phép đổi mật khẩu mà không yêu cầu xác nhận mật khẩu cũ. Điều này cực kỳ nguy hiểm nếu một người khác vô tình sử dụng máy tính của người dùng đã đăng nhập.
Hôm nay, chúng ta sẽ xây dựng endpoint PUT /api/user/change-password với đầy đủ các bước xác thực "chặt chẽ" nhất.
1. Tầng Model: Kiểm tra và Cập nhật
Tại User.php, chúng ta cần thêm logic để so khớp mật khẩu hiện tại và thực hiện ghi đè mật khẩu mới đã được hash.
File: app/Models/User.php (Bổ sung)
<?php
namespace App\Models;
// ... (Giữ nguyên các thuộc tính và constructor)
/**
* Xác minh mật khẩu hiện tại của người dùng
*/
public function verifyPassword($userId, $inputPassword) {
$user = $this->findById($userId); // Hàm findById đã viết ở phần 6
if (!$user) return false;
return password_verify($inputPassword, $user['password']);
}
/**
* Cập nhật mật khẩu mới vào Database
*/
public function changePassword($userId, $newHashedPassword) {
$stmt = $this->db->prepare("UPDATE Users SET password = ?, updated_at = NOW() WHERE id = ?");
return $stmt->execute([$newHashedPassword, $userId]);
}
2. Tầng Controller: Điều phối logic xác thực
Chúng ta sẽ mở rộng UserController để xử lý các bước kiểm tra dữ liệu đầu vào (Validation) và xác thực người dùng qua JWT.
File: app/Controllers/UserController.php(Bổ sung method)
<?php
namespace App\Controllers;
use App\Core\Response;
use App\Models\User;
use App\Middleware\AuthMiddleware;
class UserController
{
// ... (Hàm updateProfile cũ ở phần 6)
public function changePassword() {
$data = json_decode(file_get_contents("php://input"), true);
// 1. Kiểm tra các trường bắt buộc
$required = ['old_password', 'new_password', 'confirm_password'];
$missing = array_filter($required, fn($f) => empty($data[$f]));
if (!empty($missing)) {
Response::json(['error' => 'Vui lòng nhập đầy đủ các thông tin'], 422);
}
// 2. Kiểm tra mật khẩu mới và xác nhận có khớp nhau không
if ($data['new_password'] !== $data['confirm_password']) {
Response::json(['error' => 'Mật khẩu mới và xác nhận mật khẩu không khớp'], 422);
}
// 3. Kiểm tra độ dài mật khẩu (Ví dụ tối thiểu 6 ký tự)
if (strlen($data['new_password']) < 6) {
Response::json(['error' => 'Mật khẩu mới phải có ít nhất 6 ký tự'], 422);
}
/**
* 4. Xác thực JWT để lấy User ID
*/
$userPayload = AuthMiddleware::check();
$userId = $userPayload->sub;
$userModel = new User();
// 5. Kiểm tra mật khẩu cũ có đúng không
if (!$userModel->verifyPassword($userId, $data['old_password'])) {
Response::json(['error' => 'Mật khẩu hiện tại không chính xác'], 401);
}
// 6. Hash mật khẩu mới và cập nhật
$hashed = password_hash($data['new_password'], PASSWORD_DEFAULT);
$userModel->changePassword($userId, $hashed);
Response::json(['message' => 'Đổi mật khẩu thành công!']);
}
}
3. Cập nhật Route (index.php)
Đừng quên đăng ký Endpoint này vào hệ thống Router của chúng ta.
File: public/index.php
// ...
if ($uri === '/api/user/profile' && $method === 'PUT') {
$userController->updateProfile();
}
// Thêm route mới cho Change Password
elseif ($uri === '/api/user/change-password' && $method === 'PUT') {
$userController->changePassword();
}
// ...
4. Kiểm thử với Curl
Hãy sử dụng Token JWT mà bạn nhận được sau khi đăng nhập để thực hiện request này:
curl -X PUT http://localhost:8000/api/user/change-password \
-H "Authorization: Bearer {JWT_TOKEN_CUA_BAN}" \
-H "Content-Type: application/json" \
-d '{
"old_password": "mat_khau_cu_123",
"new_password": "new_password_456",
"confirm_password": "new_password_456"
}'
Góc nhìn chuyên gia: Nâng cấp bảo mật
Trong các hệ thống lớn, sau khi người dùng đổi mật khẩu, bạn nên cân nhắc thực hiện thêm:
Vô hiệu hóa Token cũ: Nếu bạn lưu Token trong Database (Stateful), hãy xóa toàn bộ Token cũ để buộc người dùng phải đăng nhập lại trên mọi thiết bị.
Email thông báo: Ngay sau khi đổi mật khẩu thành công, hãy gửi một email thông báo: "Mật khẩu của bạn vừa được thay đổi. Nếu không phải bạn làm điều này, hãy liên hệ ngay với chúng tôi". Điều này giúp người dùng phát hiện sớm các hành vi xâm nhập.
Password Complexity: Sử dụng Regex để bắt buộc mật khẩu phải có chữ hoa, chữ thường và ký tự đặc biệt để tăng độ khó cho các cuộc tấn công Brute-force.
Lời kết
Vậy là mảnh ghép về Quản lý mật khẩu đã hoàn thiện. API của chúng ta giờ đây không chỉ mạnh về tính năng mà còn vững về bảo mật.
All rights reserved