[Series] Xây dựng RESTful API từ con số 0 với PHP Thuần & MVC - Phần 9: Hệ thống Phân quyền (Permissions) & Middleware nâng cao
Chào các bạn, mình đã quay trở lại!
Trong các dự án thực tế (như hệ thống vận hành tại Hasaki), việc phân quyền không chỉ dừng lại ở Role. Chúng ta cần sự linh hoạt: Hôm nay Role "Editor" được quyền edit_post, nhưng ngày mai có thể được thêm quyền delete_post. Nếu chỉ check theo Role Name, bạn sẽ phải sửa code liên tục.
Giải pháp? Phân quyền dựa trên tính năng (Permission-based Access Control). Hãy cùng bắt đầu nhé!
1. Kiến trúc Cơ sở dữ liệu
Chúng ta cần hai bảng mới để quản lý danh sách quyền và mối liên kết giữa quyền với vai trò.
-- Danh sách các quyền (ví dụ: 'edit_post', 'delete_user',...)
CREATE TABLE Permissions (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) UNIQUE,
created_at DATETIME
);
-- Bảng trung gian gán quyền cho vai trò (Many-to-Many)
CREATE TABLE Role_Permissions (
role_id INT,
permission_id INT,
PRIMARY KEY (role_id, permission_id),
FOREIGN KEY (role_id) REFERENCES Roles(id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES Permissions(id) ON DELETE CASCADE
);
2. Tầng Model: Quản lý Quyền hạn (Permission.php)
Model này sẽ chịu trách nhiệm chính trong việc truy vấn và gán quyền.
File: app/Models/Permission.php
<?php
namespace App\Models;
use App\Core\Database;
class Permission {
protected $db;
public function __construct() {
$this->db = Database::getInstance();
}
public function getAll() {
return $this->db->query("SELECT * FROM Permissions ORDER BY id DESC")->fetchAll();
}
public function create($name) {
$stmt = $this->db->prepare("INSERT INTO Permissions (name, created_at) VALUES (?, NOW())");
$stmt->execute([$name]);
return $this->db->lastInsertId();
}
/**
* Gán danh sách quyền cho một vai trò
*/
public function assignToRole($roleId, array $permissionIds) {
// 1. Xoá các quyền cũ của vai trò này để làm sạch dữ liệu
$this->db->prepare("DELETE FROM Role_Permissions WHERE role_id = ?")->execute([$roleId]);
// 2. Gán các quyền mới
$stmt = $this->db->prepare("INSERT INTO Role_Permissions (role_id, permission_id) VALUES (?, ?)");
foreach ($permissionIds as $pid) {
$stmt->execute([$roleId, $pid]);
}
}
/**
* Lấy danh sách tên quyền mà một vai trò đang sở hữu
*/
public function getPermissionsByRole($roleId) {
$stmt = $this->db->prepare("
SELECT p.id, p.name
FROM Permissions p
JOIN Role_Permissions rp ON p.id = rp.permission_id
WHERE rp.role_id = ?
");
$stmt->execute([$roleId]);
return $stmt->fetchAll();
}
}
3. Tầng Controller: Quản lý API Permissions
Chúng ta tạo PermissionController để expose các endpoint ra bên ngoài.
File: app/Controllers/PermissionController.php
<?php
namespace App\Controllers;
use App\Core\Response;
use App\Models\Permission;
class PermissionController
{
public function index() {
$perms = (new Permission())->getAll();
Response::json(['permissions' => $perms]);
}
public function create() {
$data = json_decode(file_get_contents("php://input"), true);
if (empty($data['name'])) {
Response::json(['error' => 'Permission name is required'], 422);
}
$id = (new Permission())->create($data['name']);
Response::json(['message' => 'Quyền mới đã được tạo', 'id' => $id], 201);
}
public function assignToRole($roleId) {
$data = json_decode(file_get_contents("php://input"), true);
if (empty($data['permissions']) || !is_array($data['permissions'])) {
Response::json(['error' => 'Danh sách permissions phải là một mảng ID'], 422);
}
(new Permission())->assignToRole($roleId, $data['permissions']);
Response::json(['message' => 'Đã cập nhật quyền cho vai trò']);
}
public function getByRole($roleId) {
$perms = (new Permission())->getPermissionsByRole($roleId);
Response::json(['role_id' => $roleId, 'permissions' => $perms]);
}
}
4. Bonus: Middleware kiểm tra quyền "Xịn"
Đây là phần quan trọng nhất! Thay vì check quyền thủ công trong từng hàm, chúng ta tạo một Middleware để dùng ở bất cứ đâu.
File: app/Middleware/PermissionMiddleware.php
<?php
namespace App\Middleware;
use App\Core\Response;
use App\Models\Permission;
use App\Models\User;
class PermissionMiddleware {
/**
* Kiểm tra người dùng hiện tại có quyền cụ thể nào đó không
*/
public static function check($requiredPermission) {
// 1. Xác thực JWT để lấy thông tin user
$userToken = AuthMiddleware::check();
// 2. Lấy thông tin user (bao gồm role_id) từ DB
$userModel = new User();
$userData = $userModel->findById($userToken->sub);
// 3. Lấy danh sách quyền của Role đó
$permModel = new Permission();
$permissions = $permModel->getPermissionsByRole($userData['role_id']);
// 4. Trích xuất mảng tên quyền và kiểm tra
$permNames = array_column($permissions, 'name');
if (!in_array($requiredPermission, $permNames)) {
Response::json([
'error' => 'Permission denied',
'message' => "Bạn không có quyền [$requiredPermission] để thực hiện hành động này"
], 403);
}
return true;
}
}
5. Kiểm thử (Test with Curl)
Tạo một quyền mới:
curl -X POST http://localhost:8000/api/permissions \
-H "Content-Type: application/json" \
-d '{"name":"edit_post"}'
Gán quyền (ID: 1, 2) cho vai trò Admin (ID: 1):
curl -X POST http://localhost:8000/api/roles/1/permissions \
-H "Content-Type: application/json" \
-d '{"permissions": [1, 2]}'
Lời kết & Bước tiếp theo
Vậy là chúng ta đã hoàn thiện một hệ thống phân quyền RBAC (Role-Based Access Control) cực kỳ bài bản. Từ nay, bất kỳ Controller nào cần bảo mật, bạn chỉ cần gọi:
PermissionMiddleware::check('delete_user');
Hãy để lại bình luận cho mình biết nhé! Đừng quên Upvote để ủng hộ tinh thần blogger. Chúc các bạn code vui vẻ!
All rights reserved