Skip to content

Mô hình phân quyền

Quyền trong framework là URI hành động, so khớp chính xác:

/attendance-summary/search
/orders/create
/orders/update

Không phải mã dạng order.read, không có ký tự đại diện, không suy diễn theo tiền tố. Có /orders/search không đồng nghĩa với có /orders/search/export.

Quyền từ đâu ra

IAM userInfo → body.authorization[] → lọc type='web' → lấy field .uri → user.permissions

Danh sách này được nạp lại mỗi lần khởi tạo app (qua GET /auth/session), nên quyền vừa đổi ở IAM có hiệu lực ngay ở lần tải trang kế tiếp. Cách bóc dữ liệu cấu hình được — xem Cấu hình IAM.

Chuẩn hoá trước khi so khớp

URI được chuẩn hoá cả hai phía trước khi so:

  • Bỏ phần query: /orders/search?tab=new/orders/search.
  • Bỏ dấu / thừa ở cuối: /orders/search//orders/search.

Ngoài hai việc đó thì so khớp là tuyệt đối, phân biệt chữ hoa chữ thường.

usePermission

ts
const { can, canAccess, canAll, canAny, hasRole, hasAnyRole, filterByPermission } = usePermission()
HàmTrả về
can(uri)Có đúng quyền đó
canAccess(uri)Giống can, tên gọi cho ngữ cảnh route/trang
canAll(uris)tất cả quyền trong danh sách
canAny(uris)ít nhất một quyền
hasRole(role)Có role (so khớp chính xác mã role)
hasAnyRole(roles)Có ít nhất một role
filterByPermission(items)Lọc mảng { permission?, children? }, bỏ item không có quyền; item không khai permission luôn được giữ

Kết quả là computed theo user, nên đăng nhập/đăng xuất thì giao diện tự cập nhật.

ts
if (can('/orders/update')) {
  // ...
}

const menu = filterByPermission([
  { label: 'Đơn hàng', path: '/orders', permission: '/orders/search' },
  { label: 'Báo cáo', path: '/reports', permission: '/reports/view' },
])

Role và super-role

Role (user.roles) là mã như AIWSP_ADMIN, dùng khi quyết định phụ thuộc vai trò chứ không phải một hành động cụ thể — ví dụ hiện khối thống kê chỉ dành cho quản lý.

Một số role được phép bỏ qua mọi kiểm tra quyền:

bash
NUXT_PUBLIC_AUTH_SUPER_ROLES=OAP_ADMIN,org_admin

Người dùng có role trong danh sách thì can(), canAll(), canAny() luôn trả true. Dùng cho tài khoản quản trị hệ thống; đừng biến nó thành cách né việc khai quyền.

Ưu tiên quyền, hạn chế role

Quyền mô tả hành động nên đọc code là hiểu ("cần quyền tạo đơn"). Role mô tả người nên dễ phình ra thành chuỗi if khó bảo trì. Chỉ dùng hasRole khi thực sự không có URI tương ứng.

Thống nhất URI với backend

Danh sách URI phải là một giữa IAM, backend và frontend. Thực tế hay lệch ở những chỗ này:

Rủi roCách tránh
Frontend khai /orders/search, IAM phát /order/searchLấy danh sách URI trực tiếp từ IAM, đừng gõ lại theo trí nhớ
Một hành động có hai URI ở hai màn hìnhĐặt hằng số dùng chung trong dự án
Backend không kiểm, chỉ frontend ẩn nútBackend phải kiểm mọi endpoint
ts
// app/utils/permissions.ts — khai một lần, dùng khắp nơi
export const PERM = {
  ordersSearch: '/orders/search',
  ordersCreate: '/orders/create',
  ordersUpdate: '/orders/update',
} as const
ts
definePageMeta({ permissions: [PERM.ordersSearch] })

Kiểm tra bằng unit test

Lõi kiểm quyền là hàm thuần, không phụ thuộc Nuxt hay Vue — test thẳng được:

ts
import { createPermissionChecker } from '@tasco/composables'

const checker = createPermissionChecker(['/orders/search'], ['USER'], ['OAP_ADMIN'])

checker.can('/orders/search')    // true
checker.can('/orders/create')    // false
checker.can('/orders/search/')   // true — chuẩn hoá dấu / cuối

Ranh giới

WARNING

Toàn bộ nội dung trang này chạy trong trình duyệt và chỉ quyết định giao diện hiển thị gì. Người dùng vẫn gọi được API bằng công cụ khác. Bảo mật thật nằm ở backend: mỗi endpoint tự kiểm tra quyền trên token nhận được.

Liên quan