0

[Series] Xây dựng RESTful API từ con số 0 với PHP Thuần & MVC - Phần 4: Tính năng Quên & Đặt lại mật khẩu

Chào các bạn, mình đã quay trở lại!

Quên mật khẩu là một kịch bản không thể tránh khỏi trong bất kỳ hệ thống nào. Về cơ bản, chúng ta không thể "trả lại" mật khẩu cũ cho người dùng (vì đã hash mất rồi!). Thay vào đó, chúng ta sẽ cấp cho họ một "vé thông hành tạm thời" (Reset Token) qua Email để họ thiết lập mật khẩu mới.

image.png

1. Chuẩn bị Cơ sở dữ liệu

Trong bảng Users mà chúng ta đã thiết kế ở Phần 1, chúng ta sẽ tận dụng trường activation_token để lưu mã Reset. Nếu dự án của bạn chưa có, hãy thêm một trường tương tự (ví dụ: reset_token) vào DB nhé.

2. Tầng Model: Quản lý Token và Mật khẩu

Chúng ta cần bổ sung các phương thức để tìm kiếm người dùng qua Token và cập nhật mật khẩu mới.

File: app/Models/User.php (Cập nhật)

<?php
namespace App\Models;

use App\Core\Database;
use PDO;

class User {
    protected $db;

    public function __construct() {
        $this->db = Database::getInstance();
    }

    // ... (Giữ nguyên các hàm cũ)

    public function findByEmail($email) {
        $stmt = $this->db->prepare("SELECT * FROM Users WHERE email = ?");
        $stmt->execute([$email]);
        return $stmt->fetch();
    }

    public function setResetToken($email, $token) {
        $stmt = $this->db->prepare("UPDATE Users SET activation_token = ? WHERE email = ?");
        return $stmt->execute([$token, $email]);
    }

    public function findByToken($token) {
        $stmt = $this->db->prepare("SELECT * FROM Users WHERE activation_token = ?");
        $stmt->execute([$token]);
        return $stmt->fetch();
    }

    public function updatePassword($userId, $hashedPassword) {
        // Sau khi đổi pass thành công, ta xóa token để tránh dùng lại (security best practice)
        $stmt = $this->db->prepare("UPDATE Users SET password = ?, activation_token = NULL WHERE id = ?");
        return $stmt->execute([$hashedPassword, $userId]);
    }
}

3. Tầng Core: Mô phỏng gửi Email (Mailer)

Trong môi trường thực tế, bạn sẽ dùng các thư viện như PHPMailer hoặc SwiftMailer. Để đơn giản hóa cho bài học này, chúng ta sẽ tạo một lớp mô phỏng việc gửi mail.

File: app/Core/Mailer.php

<?php
namespace App\Core;

class Mailer {
    public static function send($to, $subject, $body) {
        /**
         * Ở môi trường Production, bạn sẽ dùng hàm mail() 
         * hoặc kết nối tới SMTP Server (Gmail, SendGrid, Mailgun...)
         */
        echo "--------------------------------------------------\n";
        echo "📧 EMAIL SIMULATION\n";
        echo "To: $to\n";
        echo "Subject: $subject\n";
        echo "Content: $body\n";
        echo "--------------------------------------------------\n";
    }
}

4. Tầng Controller: Xử lý Logic Reset

Đây là nơi "nhào nặn" chính. Chúng ta cần 2 hàm: Một để gửi yêu cầu reset, và một để thực hiện đổi mật khẩu khi có token.

File: app/Controllers/PasswordController.php

<?php
namespace App\Controllers;

use App\Core\Response;
use App\Core\Mailer;
use App\Models\User;

class PasswordController
{
    /**
     * Bước 1: Gửi yêu cầu Reset
     */
    public function forgotPassword() {
        $data = json_decode(file_get_contents("php://input"), true);
        
        if (empty($data['email'])) {
            Response::json(['error' => 'Vui lòng nhập Email'], 422);
        }

        $userModel = new User();
        $user = $userModel->findByEmail($data['email']);

        if (!$user) {
            // Bảo mật: Đôi khi người ta trả về 200 luôn để tránh lộ email có tồn tại hay không
            Response::json(['error' => 'Email không tồn tại trong hệ thống'], 404);
        }

        // Tạo token ngẫu nhiên cực mạnh
        $token = bin2hex(random_bytes(32));
        $userModel->setResetToken($data['email'], $token);

        // Tạo link (Thực tế link này sẽ dẫn về trang Frontend của bạn)
        $resetLink = "http://localhost:8000/api/reset-password?token=$token";

        Mailer::send(
            $data['email'], 
            'Reset Your Password', 
            "Chào bạn, vui lòng click vào link sau để đổi mật khẩu: $resetLink"
        );

        Response::json(['message' => 'Link reset mật khẩu đã được gửi vào email của bạn']);
    }

    /**
     * Bước 2: Thực hiện đổi mật khẩu
     */
    public function resetPassword() {
        $data = json_decode(file_get_contents("php://input"), true);
        
        if (empty($data['token']) || empty($data['new_password'])) {
            Response::json(['error' => 'Token và mật khẩu mới là bắt buộc'], 422);
        }

        $userModel = new User();
        $user = $userModel->findByToken($data['token']);

        if (!$user) {
            Response::json(['error' => 'Token không hợp lệ hoặc đã hết hạn'], 400);
        }

        $hashedPassword = password_hash($data['new_password'], PASSWORD_DEFAULT);
        $userModel->updatePassword($user['id'], $hashedPassword);

        Response::json(['message' => 'Mật khẩu đã được cập nhật thành công!']);
    }
}

5. Cập nhật Route (index.php)

Mở "cánh cổng" cho hai API mới này nào!

File: public/index.php

<?php
require_once __DIR__ . '/../vendor/autoload.php';

use App\Controllers\PasswordController;

$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
$passController = new PasswordController();

// ... (Các route cũ ở các phần trước)

if ($uri === '/api/forgot-password' && $method === 'POST') {
    $passController->forgotPassword();
} elseif ($uri === '/api/reset-password' && $method === 'POST') {
    $passController->resetPassword();
}

6. Kiểm thử tính năng (Test with Curl)

Gửi yêu cầu reset:

curl -X POST http://localhost:8000/api/forgot-password \
  -H "Content-Type: application/json" \
  -d '{"email":"test@gmail.com"}'

Xem console để lấy Token từ Email giả lập.

Đặt lại mật khẩu mới: (Thay abc123 bằng token bạn vừa nhận được)

curl -X POST http://localhost:8000/api/reset-password \
  -H "Content-Type: application/json" \
  -d '{"token":"abc123_vừa_lấy", "new_password":"new_secure_password"}'

Lời kết & Nâng cấp bảo mật Hệ thống của chúng ta đã khá hoàn thiện về mặt logic. Tuy nhiên, để đưa lên Production, bạn nên cân nhắc:

Token Expiry: Thêm trường reset_token_expires_at để token chỉ có hiệu lực trong 15-30 phút.

PHPMailer: Sử dụng thư viện thật để gửi mail qua SMTP của Gmail hoặc các dịch vụ như SendGrid.

Password Strength: Validate độ mạnh mật khẩu (phải có chữ hoa, số, ký tự đặc biệt).

Vậy là chúng ta đã đi qua 4 phần cực kỳ quan trọng của một Backend API. Bạn cảm thấy thế nào?


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí