Series Authorization Thực chiến #4: Vượt rào RBAC – Nghệ thuật Phân quyền Cấp độ Dữ liệu (Row-Level Security)
1. Lỗ hổng của RBAC và Ác mộng "Bùng nổ vai trò"
Nhiều lập trình viên khi đối mặt với bài toán "Vé ga nào, ga đó hoàn" thường chọn cách giải quyết sai lầm nhất: Đẻ thêm quyền mới trong Database.
Thay vì một quyền refund_ticket, họ tạo ra refund_ticket_ben_thanh, refund_ticket_suoi_tien. Khi hệ thống Metro mở rộng lên 50 nhà ga, bảng permissions của bạn sẽ phình to thành hàng ngàn dòng rác. Đây gọi là hiện tượng Role Explosion (Bùng nổ vai trò).
Giải pháp chuẩn Enterprise: RBAC (Database/Middleware) chỉ làm nhiệm vụ trả lời câu hỏi: "Anh có được cấp phép làm hành động này nói chung hay không?". Để trả lời câu hỏi: "Anh có được làm hành động này trên Tài nguyên (Resource) cụ thể này hay không?", chúng ta phải dùng đến Policy.
2. Tư duy Policy: Đưa Thuộc tính (Attribute) lên bàn cân
Để Policy hoạt động, bảng users và bảng tickets (vé) của bạn cần có các thuộc tính để đối chiếu.
- Nhân viên A có thuộc tính: station_id = 1 (Ga Bến Thành).
- Vé số #999 có thuộc tính: station_id = 2 (Ga Suối Tiên).
Luật của Policy cực kỳ đơn giản: Chỉ cho phép nếu user.station_id == ticket.station_id hoặc User đó là Giám đốc toàn tuyến (Super Admin).
3. Thực chiến: Viết Policy bảo vệ Tài nguyên
Hãy mở Terminal và tạo một Policy gắn liền với Model Ticket (Vé):
d-artisan make:policy TicketPolicy --model=Ticket
Laravel sẽ sinh ra file app/Policies/TicketPolicy.php. Chúng ta sẽ viết logic bảo vệ cho hành động refund (Hoàn tiền) vào đây:
namespace App\Policies;
use App\Models\User;
use App\Models\Ticket;
use Illuminate\Auth\Access\HandlesAuthorization;
class TicketPolicy
{
use HandlesAuthorization;
/**
* Hàm này luôn chạy trước tiên.
* Dùng để đặc cách cho Super Admin (Giám đốc toàn tuyến).
*/
public function before(User $user, $ability)
{
// Nếu là Super Admin, cho phép làm mọi thứ, khỏi cần check các luật bên dưới
if ($user->hasRole('super_admin')) {
return true;
}
}
/**
* Luật cho hành động Hoàn tiền vé
*/
public function refund(User $user, Ticket $ticket)
{
// 1. Kiểm tra User có được gán quyền refund_ticket trong DB không (Chính là Middleware)
// Lưu ý: Thường bước này đã được Middleware cản rồi, nhưng check lại ở đây cho chắc cú.
if (!$user->hasPermission('refund_ticket')) {
return false;
}
// 2. Row-level Security: So sánh thuộc tính dữ liệu
// User chỉ được hoàn vé của chính Ga mình quản lý
if ($user->station_id !== $ticket->station_id) {
return false;
}
// 3. Ràng buộc nghiệp vụ (Business Logic)
// Chỉ được hoàn tiền những vé được xuất trong vòng 24 giờ qua
if ($ticket->created_at->diffInHours(now()) > 24) {
return false;
}
return true;
}
}
Nhìn vào đoạn code trên, bạn sẽ thấy sự thanh lịch tuyệt đối. Mọi luật lệ kinh doanh (hết hạn 24h, khác ga, đặc cách giám đốc) đều được nhốt gọn gàng vào trong một class duy nhất.
4. Kích hoạt lớp giáp Policy trong Controller
Bây giờ, hãy quay lại TicketController – nơi xử lý API hoàn tiền vé. Chúng ta sẽ "bóp cò" Policy ngay sau khi query dữ liệu từ DB lên và trước khi thực hiện tính toán tiền bạc.
namespace App\Http\Controllers;
use App\Models\Ticket;
use Illuminate\Http\Request;
class TicketController extends Controller
{
public function refund(Request $request, $ticketId)
{
// 1. Tìm vé trong Database
$ticket = Ticket::findOrFail($ticketId);
// 2. Kích hoạt Policy chặn quyền (Row-Level Security)
// Hàm authorize() sẽ tự động ném ra lỗi 403 Forbidden nếu hàm refund() trong Policy trả về false
$this->authorize('refund', $ticket);
// 3. Nếu lọt qua được bước 2, chắc chắn 100% User này hợp lệ với cái vé này
// Bắt đầu logic hoàn tiền...
$ticket->status = 'refunded';
$ticket->save();
// Ghi log, gọi API ngân hàng, v.v...
return response()->json(['message' => 'Hoàn tiền thành công!']);
}
}
5. Tại sao Kiến trúc này là Bất Bại?
Hãy điểm lại luồng di chuyển của hệ thống Metro lúc này:
- AuthN: Khách hoặc Nhân viên gửi Request có Token hợp lệ. -> Biết được là ai.
- AuthZ Middleware (Redis): Kiểm tra cực nhanh xem Nhân viên này có cái "mác" refund_ticket hay không. -> Chặn ngay ngoài cửa nếu không có chuyên môn.
- Controller: Lôi thông tin cái vé cụ thể ra.
- Policy (ABAC): Đặt User và cái Vé lên bàn cân. Khác Ga? Quá 24h? -> Bắn ra lỗi 403 ngay lập tức.
Xử lý Logic: Cập nhật Database an toàn tuyệt đối.
Kiến trúc này tách biệt hoàn toàn giữa Hạ tầng (Middleware/Redis) và Nghiệp vụ (Policy). Ngày mai luật thay đổi thành "được hoàn trong 48h", bạn chỉ vào đúng một file TicketPolicy để sửa số 24 thành 48. Controller hoàn toàn không bị chạm tới (Open/Closed Principle).
Lời kết cho Series Authorization Chúc mừng bạn! Bốn bài học vừa qua đã trang bị cho bạn toàn bộ "vũ khí" phân quyền từ cấp độ Macro (Kiến trúc AuthN/AuthZ, Database RBAC) đến cấp độ Micro (Middleware Redis, Row-Level Security Policies).
Nắm vững hệ thống này, bạn có thể tự tin xây dựng tính năng phân quyền cho những nền tảng SaaS phức tạp, hệ thống ERP nội bộ, hay hệ thống tài chính với hàng ngàn người dùng đa chi nhánh mà không sợ lọt dữ liệu.
All rights reserved