Series Authorization Thực chiến #3: Code Middleware "Lính gác" – Chặn đứng mọi request vượt quyền
1. Luồng di chuyển của một Request an toàn
Hãy hình dung một luồng API đi vào hệ thống AFC của chúng ta sẽ trải qua các chốt chặn sau:
- Client (App/Web): Gửi Request kèm theo một chuỗi Token (JWT hoặc Session).
- Chốt chặn 1 (AuthN Middleware): Kiểm tra xem Token có hợp lệ không. Nếu đúng, giải mã ra user_id. (Trả lời câu hỏi: Bạn là ai?)
- Chốt chặn 2 (AuthZ Middleware): Chốt chặn mà chúng ta sẽ viết hôm nay. Nó sẽ kiểm tra xem user_id này có quyền thực hiện hành động hiện tại không. (Trả lời câu hỏi: Bạn được phép làm gì?)
- Controller: Nơi xử lý nghiệp vụ kinh doanh (Tính toán, gọi Database lưu giao dịch).
Nếu không có chốt chặn số 3, bất kỳ ai lọt qua chốt số 1 đều có thể "phá" nát hệ thống của bạn!
2. Chuẩn bị đạn dược: Load Permission siêu tốc với Redis
Như đã cảnh báo ở bài trước, chúng ta tuyệt đối không chạy câu lệnh SQL JOIN qua 5 bảng mỗi khi kiểm tra quyền. Tốc độ của API phải được tính bằng mili-giây. Chúng ta sẽ lấy toàn bộ quyền của User ném vào Redis ngay lúc đăng nhập.
Đây là một đoạn logic mẫu (viết bằng PHP/Laravel, nhưng tư duy áp dụng tương tự cho Golang hay Node.js):
// Logic chạy 1 lần duy nhất khi User đăng nhập thành công
public function cacheUserPermissions($user)
{
// Lấy tất cả tên quyền (slug) của user thông qua Role
$permissions = $user->roles()
->with('permissions')
->get()
->pluck('permissions.*.slug')
->flatten()
->unique()
->toArray();
// Lưu vào Redis, key là prefix + user_id, tồn tại trong 24h
$cacheKey = 'user_permissions_' . $user->id;
Redis::setex($cacheKey, 86400, json_encode($permissions));
}
Lúc này, trong Redis của chúng ta đã có một mảng rất nhẹ nhàng: ['view_transactions', 'refund_ticket', 'sync_offline_data'].
3. Viết Middleware Phân Quyền (The Bouncer)
Bây giờ là lúc tạo ra người gác cổng. Chức năng của Middleware này rất đơn giản: Nhận vào tên của một Quyền cần thiết, so sánh nó với mảng quyền trong Redis. Có thì cho đi tiếp (next), không có thì đuổi về (403 Forbidden).
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Redis;
class CheckPermission
{
/**
* $requiredPermission là tham số ta sẽ truyền vào từ Route
*/
public function handle(Request $request, Closure $next, $requiredPermission)
{
$user = $request->user();
// 1. Nếu chưa đăng nhập (Lọt qua AuthN bị lỗi), đá văng ngay
if (!$user) {
return response()->json(['message' => 'Unauthorized'], 401);
}
// 2. Lấy danh sách quyền từ Redis (Cực kỳ nhanh, O(1))
$cacheKey = 'user_permissions_' . $user->id;
$cachedPermissions = Redis::get($cacheKey);
if (!$cachedPermissions) {
// Fallback: Nếu Redis sập hoặc mất cache, tính toán lại từ DB
// (Gọi lại hàm cacheUserPermissions ở mục 2)
// ...
return response()->json(['message' => 'Lỗi đồng bộ quyền'], 500);
}
$permissionsArray = json_decode($cachedPermissions, true);
// 3. Quyết định sinh tử: Mảng quyền của User có chứa quyền hệ thống yêu cầu không?
if (!in_array($requiredPermission, $permissionsArray)) {
return response()->json([
'error' => 'Forbidden',
'message' => "Bạn không có quyền: {$requiredPermission}. Vui lòng liên hệ Quản lý ga."
], 403);
}
// 4. Cho phép Request đi tiếp vào Controller
return $next($request);
}
}
4. Triển khai đội hình gác cổng vào Routes
Vũ khí đã rèn xong, giờ là lúc mang ra trấn giữ các API. Bạn chỉ cần gắn Middleware này vào các Route tương ứng, truyền cho nó cái "mật khẩu" (Quyền) mà nó cần kiểm tra.
// File: routes/api.php
Route::middleware(['auth:api'])->group(function () {
// Bất kỳ ai đăng nhập cũng xem được profile cá nhân
Route::get('/profile', [UserController::class, 'profile']);
// Chỉ người có quyền 'sync_offline_data' mới được gọi API này
Route::post('/afc/sync', [TransactionController::class, 'syncData'])
->middleware('permission:sync_offline_data');
// Chỉ người có quyền 'refund_ticket' mới được hoàn tiền vé
Route::post('/tickets/{id}/refund', [TicketController::class, 'refund'])
->middleware('permission:refund_ticket');
// Chỉ Quản lý có quyền xem log hệ thống thiết bị Metro
Route::get('/system/logs', [SystemController::class, 'logs'])
->middleware('permission:view_system_logs');
});
- Sức mạnh của kiến trúc "Decoupled" (Tách rời) Nhìn vào đoạn code routes ở trên, bạn sẽ thấy sự thanh lịch của một kiến trúc Backend Enterprise.
Bên trong TransactionController hay TicketController của bạn lúc này chỉ chứa thuần túy logic tính toán tiền bạc, đồng bộ dữ liệu. Không hề có một dòng if ($user->role == 'admin') nào làm bẩn code.
Hơn thế nữa, nếu ngày mai bộ phận vận hành đổi luật: "Cho phép Kế toán cũng được hoàn tiền vé". Bạn KHÔNG cần phải sửa một dòng code nào cả! Bạn chỉ cần vào giao diện Admin Panel (hoặc chọc thẳng vào Database), thêm quyền refund_ticket cho vai trò Kế toán. Ngay lập tức, luồng API sẽ tự động mở cửa cho bộ phận này thông qua Redis.
Đó chính là đỉnh cao của sự linh hoạt trong phân quyền.
Tổng kết Bài 3
Chúng ta đã chuyển hóa lý thuyết thiết kế Database thành những dòng code bảo vệ thực tế. Bằng việc kết hợp Middleware và sức mạnh caching của Redis, hệ thống API của bạn giờ đây đã có một lớp giáp chống đạn vững chắc, không bao giờ lo bị quá tải Database khi rà soát quyền hạn, và code nghiệp vụ thì luôn sạch sẽ.
Ở bài tiếp theo, chúng ta sẽ bước vào những góc tối khó nhằn nhất của Phân quyền: Cấp độ Dữ liệu (Row-level Security / ABAC). Giải quyết bài toán: Quản lý Ga A có quyền "Hoàn tiền vé", nhưng hệ thống phải chặn không cho anh ta hoàn tiền cái vé được bán ra ở Ga B! Cùng đón xem nhé.
All Rights Reserved