Tự xây hệ thống RBAC siêu nhẹ bằng JSON Permission và Laravel Gates (Không dùng Package)
hôm nay, để đáp ứng yêu cầu "hoàn toàn mới", mình sẽ hướng dẫn bạn một trường phái thiết kế RBAC (Role-Based Access Control) khác biệt hoàn toàn: Native RBAC với JSON Permissions. Thay vì tạo ra 5-6 bảng trung gian chằng chịt, chúng ta sẽ tự xây dựng một hệ thống phân quyền cực kỳ gọn nhẹ, tốc độ bàn thờ (vì không phải JOIN nhiều bảng) bằng cách tận dụng sức mạnh của cột JSON trong MySQL/PostgreSQL và cơ chế Gates/Policies cốt lõi của Laravel.
Kiến trúc này đặc biệt phù hợp cho các hệ thống Microservices hoặc API cần tốc độ phản hồi tính bằng mili-giây. Lên sóng ngay nhé!
Lời mở đầu: Tại sao không dùng Package?
Các package phân quyền thường tạo ra một hệ thống CSDL đồ sộ (bảng roles, bảng permissions, bảng role_has_permissions, bảng model_has_roles...). Mỗi lần check quyền, DB phải thực hiện các câu lệnh JOIN rất nặng.
Nếu nghiệp vụ của bạn đã được định hình rõ ràng (Super Admin, Editor, Viewer), chúng ta có thể làm cho hệ thống "bay" nhanh hơn bằng cách:
- Lưu trực tiếp danh sách quyền vào một cột
JSONtrong bảngroles. - Dùng
Gate::before()của Laravel để can thiệp vào mọi request check quyền.
Bắt tay vào code nào!
Bước 1: Khởi tạo và Thiết kế CSDL "Siêu gọn"
Tạo dự án mới tinh:
laravel new native-rbac-api
cd native-rbac-api
1. Tạo Model & Migration cho Role:
php artisan make:model Role -m
2. Viết Migration cho Role:
Mấu chốt nằm ở cột permissions kiểu JSON.
// database/migrations/xxxx_create_roles_table.php
public function up(): void
{
Schema::create('roles', function (Blueprint $table) {
$table->id();
$table->string('name'); // Tên hiển thị (VD: Biên tập viên)
$table->string('slug')->unique(); // Mã role (VD: editor)
$table->json('permissions')->nullable(); // Mảng chứa các quyền: ["write_post", "edit_post"]
$table->timestamps();
});
}
3. Cập nhật bảng Users:
Tạo migration thêm cột role_id vào bảng users.
php artisan make:migration add_role_id_to_users_table
// database/migrations/xxxx_add_role_id_to_users_table.php
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
// Gắn foreign key, set mặc định là role user bình thường (nếu cần)
$table->foreignId('role_id')->nullable()->constrained('roles')->nullOnDelete();
});
}
Chạy php artisan migrate để tạo bảng.
Bước 2: Model Casting và Logic kiểm tra quyền
Mở Model Role.php, ép kiểu cột JSON thành mảng để thao tác dễ dàng:
// app/Models/Role.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
protected $fillable = ['name', 'slug', 'permissions'];
protected $casts = [
'permissions' => 'array', // Laravel tự động serialize/deserialize JSON <-> Array
];
public function users()
{
return $this->hasMany(User::class);
}
}
Mở Model User.php, thêm liên kết Role và viết hàm check quyền cốt lõi:
// app/Models/User.php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens;
// ... code mặc định ...
public function role()
{
return $this->belongsTo(Role::class);
}
/**
* Hàm kiểm tra user có quyền cụ thể hay không
*/
public function hasPermission(string $permission): bool
{
// Tránh lỗi null nếu user chưa có role hoặc role chưa có quyền
$permissionsArray = $this->role->permissions ?? [];
return in_array($permission, $permissionsArray);
}
}
Bước 3: Phép thuật của Laravel Gates (Gate::before)
Thay vì viết hàm check ở từng Controller, chúng ta sẽ định nghĩa một luật chung cho toàn hệ thống. Mở app/Providers/AppServiceProvider.php (hoặc AuthServiceProvider nếu dùng bản cũ):
// app/Providers/AppServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\Gate;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Gate::before sẽ chạy TRƯỚC mọi lời gọi check quyền (VD: $user->can('...'))
Gate::before(function ($user, $ability) {
// 1. Nếu là Super Admin (dựa vào slug), tự động cho phép mọi thao tác!
if ($user->role && $user->role->slug === 'super-admin') {
return true;
}
// 2. Với các Role khác, kiểm tra xem tên quyền ($ability) có nằm trong mảng JSON của Role không
if ($user->hasPermission($ability)) {
return true;
}
// Trả về null để Laravel tiếp tục xử lý các policy khác (nếu có), hoặc từ chối
return null;
});
}
}
Bước 4: Tạo dữ liệu Seeder chuẩn bị cho Test
php artisan make:seeder RbacSeeder
Vào file Seeder, ta nạp dữ liệu định hình cho 3 Role bạn yêu cầu:
// database/seeders/RbacSeeder.php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\Role;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
class RbacSeeder extends Seeder
{
public function run(): void
{
// 1. Tạo Role Viewer
$viewerRole = Role::create([
'name' => 'Người xem',
'slug' => 'viewer',
'permissions' => ['view_stats'] // Chỉ xem thống kê
]);
// 2. Tạo Role Editor
$editorRole = Role::create([
'name' => 'Biên tập viên',
'slug' => 'editor',
'permissions' => ['write_post', 'edit_post'] // Không có quyền 'delete_post'
]);
// 3. Tạo Role Super Admin
$adminRole = Role::create([
'name' => 'Quản trị viên tối cao',
'slug' => 'super-admin',
'permissions' => [] // Super Admin không cần định nghĩa quyền, Gate::before đã lo
]);
// 4. Tạo Users
User::create([
'name' => 'Anh Viewer', 'email' => 'viewer@app.com',
'password' => Hash::make('123456'), 'role_id' => $viewerRole->id
]);
User::create([
'name' => 'Chị Editor', 'email' => 'editor@app.com',
'password' => Hash::make('123456'), 'role_id' => $editorRole->id
]);
User::create([
'name' => 'Sếp Admin', 'email' => 'admin@app.com',
'password' => Hash::make('123456'), 'role_id' => $adminRole->id
]);
}
}
Chạy lệnh php artisan db:seed --class=RbacSeeder.
Bước 5: Viết API Controller và chặn quyền
Để test, ta tạo 1 Controller xử lý Login lấy Token và các endpoint nghiệp vụ.
// app/Http/Controllers/Api/SystemController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
class SystemController extends Controller
{
// API Login nhanh để lấy Token
public function login(Request $request)
{
$user = User::where('email', $request->email)->with('role')->first();
if (!$user || !Hash::check($request->password, $user->password)) {
return response()->json(['message' => 'Lỗi xác thực'], 401);
}
return response()->json([
'token' => $user->createToken('API')->plainTextToken,
'role' => $user->role->slug
]);
}
// API Xem thống kê (Yêu cầu quyền view_stats)
public function getStats()
{
return response()->json(['data' => 'Doanh thu tháng này là 1 Tỷ. (Chỉ Admin và Viewer thấy)']);
}
// API Đăng bài (Yêu cầu quyền write_post)
public function createPost()
{
return response()->json(['message' => 'Đã tạo bài viết thành công! (Chỉ Admin và Editor làm được)']);
}
// API Xóa bài (Yêu cầu quyền delete_post)
public function deletePost($id)
{
return response()->json(['message' => "Đã xóa bài viết {$id} vĩnh viễn! (Chỉ Admin làm được)"]);
}
}
Bảo vệ Route trong routes/api.php:
Thay vì dùng Middleware rườm rà, ta có thể dùng trực tiếp middleware can: của Laravel.
use App\Http\Controllers\Api\SystemController;
Route::post('/login', [SystemController::class, 'login']);
Route::middleware('auth:sanctum')->group(function () {
// Chỉ những user vượt qua Gate check 'view_stats' mới vào được
Route::get('/stats', [SystemController::class, 'getStats'])->middleware('can:view_stats');
// Yêu cầu quyền write_post
Route::post('/posts', [SystemController::class, 'createPost'])->middleware('can:write_post');
// Yêu cầu quyền delete_post
Route::delete('/posts/{id}', [SystemController::class, 'deletePost'])->middleware('can:delete_post');
});
Bước 6: Test Postman - Phân rõ giai cấp
Khởi động server: php artisan serve
Giai đoạn 1: Lấy các Token
Bạn gọi API POST /api/login 3 lần với 3 email: viewer@app.com, editor@app.com và admin@app.com (pass chung là 123456) để lấy 3 cái Token khác nhau chuẩn bị test.
Giai đoạn 2: Test quyền của Viewer (Token Viewer)
- Gọi
GET /api/stats-> 200 OK:"Doanh thu tháng này là 1 Tỷ..." - Gọi
POST /api/posts-> 403 Forbidden:"This action is unauthorized."(Bị chặn đẹp mắt vì mảng JSON của Viewer không có chữwrite_post).
Giai đoạn 3: Test quyền của Editor (Token Editor)
-
Gọi
GET /api/stats-> 403 Forbidden (Editor không có quyền xem thống kê). -
Gọi
POST /api/posts-> 200 OK:"Đã tạo bài viết thành công!..." -
Gọi
DELETE /api/posts/1-> 403 Forbidden (Hệ thống chặn ngay lập tức vì mảng JSON của Editor không có delete_post).
Giai đoạn 4: Sự bá đạo của Super Admin (Token Admin)
-
Gọi
GET /api/stats-> 200 OK. -
Gọi
POST /api/posts-> 200 OK. -
Gọi
DELETE /api/posts/1-> 200 OK: "Đã xóa bài viết 1 vĩnh viễn!...". (Lưu ý: Mảng JSON của Admin trong Database đang rỗng[], nhưng vìGate::befoređã tóm được slugsuper-admin, nó Bypass toàn bộ lớp bảo vệ và cấp quyền xanh rờn cho mọi request!)
Tóm lại
Đây là cách bạn xây dựng một hệ thống RBAC không phụ thuộc vào bất cứ package nào. Sức mạnh của cột JSON cho phép bạn thêm bớt hàng trăm quyền cho một Role mà Database vẫn chỉ có đúng 2 bảng (Users và Roles). Tốc độ query nhanh gấp nhiều lần, kiến trúc trong suốt, dễ hiểu, và đặc biệt là thỏa mãn hoàn hảo yêu cầu "phân cấp quyền hạn" ở các hệ thống Enterprise.
All Rights Reserved