0

Series Authorization Thực chiến #2: Thiết kế Database chuẩn RBAC – Xây dựng bộ khung xương sống

Chào bạn và anh em!

Ở Bài 1, chúng ta đã làm rõ ranh giới giữa AuthN (Xác thực) và AuthZ (Phân quyền), đồng thời chọn ra mô hình RBAC (Role-Based Access Control) làm kim chỉ nam cho hầu hết các bài toán nghiệp vụ hiện nay.

Hôm nay, chúng ta sẽ xắn tay áo lên và đi vào phần lõi: Thiết kế Database. Nếu thiết kế sai bảng phân quyền, hệ thống của bạn sẽ phải thực hiện những câu lệnh JOIN thảm họa mỗi lần có request, gây nghẽn cổ chai (bottleneck) nghiêm trọng khi lượng người dùng tăng cao.

Hãy đặt bối cảnh chúng ta đang xây dựng module quản lý nhân sự cho hệ thống trạm Metro (AFC).

1. Kiến trúc 5 bảng kinh điển của RBAC

Để triển khai RBAC một cách chuẩn mực và linh hoạt nhất, giới kiến trúc phần mềm luôn sử dụng mô hình 5 bảng (3 bảng chính và 2 bảng trung gian). Cấu trúc này giải quyết được bài toán Many-to-Many (Nhiều - Nhiều) giữa Người dùng, Vai trò, và Quyền hạn.

Bảng 1: users (Người dùng)

Nơi lưu trữ danh tính nhân viên (được cấp phép sau khi qua AuthN).

  • id: Khóa chính
  • username / email
  • password
  • status (Đang làm việc, Đã nghỉ...)

Bảng 2: roles (Vai trò) Định nghĩa các nhóm chức danh trong hệ thống Metro.

  • id: Khóa chính
  • name: Tên hiển thị (VD: "Quản lý Trạm", "Nhân viên Kỹ thuật", "Kế toán")
  • slug: Tên định danh chuẩn để code dùng (VD: station_manager, technician, accountant)

Bảng 3: permissions (Quyền hạn)

Danh sách toàn bộ các hành động cụ thể mà hệ thống cho phép làm. Nguyên tắc vàng: Tên permission phải cực kỳ chi tiết và gắn liền với hành động.

  • id: Khóa chính
  • name: Tên hiển thị (VD: "Đồng bộ dữ liệu Offline", "Hoàn tiền vé", "Xem lịch sử giao dịch")
  • slug: Định danh (VD: sync_offline_data, refund_ticket, view_transactions)

** Bảng 4: role_permissions (Vai trò có những Quyền gì?)**

Bảng trung gian. Một vai trò có thể có nhiều quyền, và một quyền có thể thuộc về nhiều vai trò.

  • role_id (Foreign Key)
  • permission_id (Foreign Key)

Bảng 5: user_roles (Người dùng mang Vai trò gì?)

Bảng trung gian. Một nhân sự có thể kiêm nhiệm nhiều vai trò cùng lúc (VD: Vừa là Quản lý trạm, vừa là Kiểm toán nội bộ).

  • user_id (Foreign Key)
  • role_id (Foreign Key)

2. Thực thi bằng mã SQL (MySQL/Vitess)

Dưới đây là đoạn mã DDL chuẩn mực. Lưu ý cách chúng ta đánh Index để tối ưu hóa tốc độ truy vấn, vì các bảng này được đọc (READ) cực kỳ nhiều mỗi khi có request gửi lên hệ thống.

-- Tạo bảng Roles
CREATE TABLE roles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    slug VARCHAR(100) NOT NULL UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Tạo bảng Permissions
CREATE TABLE permissions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    slug VARCHAR(100) NOT NULL UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Bảng trung gian User - Role
CREATE TABLE user_roles (
    user_id INT NOT NULL,
    role_id INT NOT NULL,
    PRIMARY KEY (user_id, role_id),
    INDEX idx_user (user_id),
    INDEX idx_role (role_id)
);

-- Bảng trung gian Role - Permission
CREATE TABLE role_permissions (
    role_id INT NOT NULL,
    permission_id INT NOT NULL,
    PRIMARY KEY (role_id, permission_id),
    INDEX idx_role (role_id),
    INDEX idx_permission (permission_id)
);

3. Vấn đề "Ngoại lệ" và Bảng thứ 6 (Tùy chọn)

Kịch bản thực tế: Nhân viên Kỹ thuật A bình thường chỉ có quyền view_system_logs. Nhưng hôm nay có sự cố khẩn cấp, Trưởng ga muốn cấp riêng cho nhân viên A quyền restart_afc_device (Khởi động lại thiết bị) chỉ trong ngày hôm nay, mà KHÔNG muốn cấp quyền này cho toàn bộ những người mang chức danh "Nhân viên Kỹ thuật" khác.

Mô hình 5 bảng ở trên bị cứng nhắc và không giải quyết được ngoại lệ này. Để giải quyết, các hệ thống lớn đẻ thêm một bảng thứ 6: user_permissions (Quyền trực tiếp).

CREATE TABLE user_permissions (
    user_id INT NOT NULL,
    permission_id INT NOT NULL,
    is_granted BOOLEAN DEFAULT TRUE, -- TRUE là cấp thêm, FALSE là tước đi
    PRIMARY KEY (user_id, permission_id)
);

Với bảng này, bạn có thể cấp thêm một quyền nóng trực tiếp cho user, hoặc thậm chí tước đi (Deny) một quyền mà Role của họ đang có (bằng cách set is_granted = FALSE).

4. Tối ưu kiến trúc truy vấn: Đừng JOIN dữ liệu liên tục!

Khi áp dụng mô hình này vào một hệ thống chịu tải cao, một sai lầm phổ biến là mỗi lần User gọi API (VD: Gọi API đồng bộ dữ liệu giao dịch), Backend lại chạy một câu lệnh SQL JOIN qua 5 bảng để kiểm tra xem User đó có quyền sync_offline_data hay không.

Cách làm này sẽ trực tiếp "giết chết" Database.

Giải pháp kiến trúc chuẩn:

  1. Lưu cache vào Redis: Khi User đăng nhập thành công (AuthN), hệ thống sẽ query Database đúng 1 lần để lấy ra một mảng danh sách tên các Permission (slugs) của User đó.
  2. Lưu trữ: Mảng này sẽ được lưu vào Redis (với key là session/token của user), hoặc được "nhúng" thẳng vào bên trong Payload của chuỗi JWT (Json Web Token) nếu danh sách quyền không quá dài.
  3. Kiểm tra cực nhanh: Ở các request tiếp theo, Middleware của ứng dụng (viết bằng Golang, Node.js hay Laravel) chỉ việc lấy mảng Permission ra từ Redis/JWT và kiểm tra in_array('sync_offline_data', $permissions). Database sẽ hoàn toàn được giải phóng khỏi gánh nặng kiểm tra quyền.

Bằng việc hiểu và tổ chức dữ liệu theo chuẩn RBAC + Ngoại lệ, kết hợp chiến lược lưu đệm (Caching) hợp lý, bạn đã có một bộ khung phân quyền bất khả chiến bại cho hệ thống Backend.

Ở Bài 3, chúng ta sẽ đưa bộ khung Database này vào trong Mã nguồn thực tế (Code). Chúng ta sẽ xây dựng các Middleware để bảo vệ các Endpoint API, đảm bảo rằng mọi request vượt quyền đều bị "bay màu" với lỗi 403 Forbidden ngay từ ngoài cửa!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí