[Series] Xây dựng RESTful API từ con số 0 với PHP Thuần & MVC - Phần 3: Bảo mật API với JWT & Middleware
Chào các bạn, mình đã trở lại!
Trong thế giới API, "Stateless" là một khái niệm then chốt. Server sẽ không nhớ bạn là ai giữa các request khác nhau. Vậy làm sao để Server biết người đang gọi API /api/user/profile chính là bạn? Câu trả lời chính là JWT.
1. Chuẩn bị "Vũ khí"
Để xử lý JWT một cách chuyên nghiệp và an toàn, chúng ta sẽ dùng thư viện firebase/php-jwt. Đây là thư viện phổ biến nhất cho PHP.
Hãy mở Terminal tại thư mục gốc của dự án và chạy lệnh:
composer require firebase/php-jwt
Tiếp theo, hãy tạo một file cấu hình hoặc khai báo hằng số cho Secret Key. Đây là chìa khóa vạn năng, tuyệt đối không được để lộ nhé!
File: config.php (hoặc khai báo trong index.php)
define('JWT_SECRET', 'your_super_secret_key_6969'); // Thay bằng một chuỗi dài và khó đoán
2. Tầng Controller: Tạo JWT khi Đăng nhập
Bây giờ, thay vì trả về một chuỗi ngẫu nhiên, chúng ta sẽ "đóng gói" thông tin user vào một JWT.
File: app/Controllers/AuthController.php (Cập nhật hàm login)
<?php
namespace App\Controllers;
use App\Models\User;
use App\Core\Response;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
class AuthController {
// ... (Hàm register giữ nguyên)
public function login() {
$input = json_decode(file_get_contents("php://input"), true);
$userModel = new User();
$user = $userModel->attemptLogin($input['email'], $input['password']);
if (!$user) {
Response::json(['error' => 'Invalid credentials'], 401);
}
// Payload: Dữ liệu bạn muốn lưu trong Token
$payload = [
'iss' => 'http://localhost', // Issuer: Nguồn phát hành
'aud' => 'http://localhost', // Audience: Đối tượng sử dụng
'iat' => time(), // Issued At: Thời điểm phát hành
'exp' => time() + (60 * 60), // Expiration: Hết hạn sau 1 giờ
'sub' => $user['id'], // Subject: ID của người dùng
'email' => $user['email']
];
// Mã hóa JWT với thuật toán HS256
$jwt = JWT::encode($payload, JWT_SECRET, 'HS256');
Response::json([
'message' => 'Login successful',
'access_token' => $jwt,
'user' => [
'id' => $user['id'],
'name' => $user['name']
]
]);
}
}
3. Tạo Middleware: "Người gác cổng"
Middleware sẽ chặn các request lại, kiểm tra xem Header có Authorization: Bearer <token> không. Nếu token hợp lệ, nó sẽ cho phép đi tiếp, nếu không sẽ "tống khứ" ngay lập tức với mã lỗi 401.
File: app/Middleware/AuthMiddleware.php
<?php
namespace App\Middleware;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use App\Core\Response;
use Exception;
class AuthMiddleware
{
public static function check()
{
// 1. Lấy Header Authorization
$headers = getallheaders();
$authHeader = $headers['Authorization'] ?? '';
// 2. Kiểm tra định dạng Bearer Token
if (!$authHeader || !str_starts_with($authHeader, 'Bearer ')) {
Response::json(['error' => 'Unauthorized - Token không được cung cấp'], 401);
}
// 3. Tách lấy chuỗi Token
$token = trim(str_replace('Bearer', '', $authHeader));
try {
// 4. Giải mã và xác thực Token
$decoded = JWT::decode($token, new Key(JWT_SECRET, 'HS256'));
// Trả về dữ liệu đã giải mã để Controller sử dụng
return $decoded;
} catch (Exception $e) {
// Token hết hạn hoặc không hợp lệ
Response::json(['error' => 'Invalid or expired token: ' . $e->getMessage()], 401);
}
}
}
4. Cấu hình Route bảo vệ (Protected Routes)
Tại file index.php, chúng ta sẽ áp dụng Middleware cho những Route "nhạy cảm".
File: public/index.php
<?php
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../config.php'; // Chứa JWT_SECRET
use App\Controllers\AuthController;
use App\Middleware\AuthMiddleware;
use App\Core\Response;
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
$controller = new AuthController();
// 1. Public Routes (Ai cũng vào được)
if ($uri === '/api/register' && $method === 'POST') {
$controller->register();
} elseif ($uri === '/api/login' && $method === 'POST') {
$controller->login();
}
// 2. Protected Routes (Phải có Token mới vào được)
elseif ($uri === '/api/user/profile' && $method === 'GET') {
$userData = AuthMiddleware::check(); // Chặn ở đây!
Response::json([
'message' => 'Truy cập thành công vào khu vực bí mật',
'your_data' => $userData
]);
}
else {
Response::json(['error' => 'Route not found'], 404);
}
5. Kiểm thử thành quả
Bước 1: Đăng nhập để lấy "Chứng minh thư" (JWT)
curl -X POST http://localhost:8000/api/login \
-H "Content-Type: application/json" \
-d '{"email":"nguyenb@gmail.com","password":"password123"}'
Bạn sẽ nhận được một chuỗi dài ngoằng trong access_token.
Bước 2: Dùng Token đó để gọi API Profile
curl -X GET http://localhost:8000/api/user/profile \
-H "Authorization: Bearer <DÁN_TOKEN_VÀO_ĐÂY>"
Tạm kết cho Phần 3
Vậy là hệ thống của chúng ta đã chuyên nghiệp hơn rất nhiều rồi đấy!
JWT: Giúp xác thực không cần Session.
Middleware: Giúp tái sử dụng code kiểm tra quyền ở nhiều nơi.
Thách thức cho bạn: Bạn có để ý thấy hàm AuthMiddleware::check() trả về $decoded không? Đó chính là thông tin User ID mà bạn có thể dùng để truy vấn thêm dữ liệu cá nhân từ Database.
All rights reserved