Giao diện
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/updateKhô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.permissionsDanh 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àm | Trả về |
|---|---|
can(uri) | Có đúng quyền đó |
canAccess(uri) | Giống can, tên gọi cho ngữ cảnh route/trang |
canAll(uris) | Có tất cả quyền trong danh sách |
canAny(uris) | Có í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_adminNgườ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 ro | Cách tránh |
|---|---|
Frontend khai /orders/search, IAM phát /order/search | Lấ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út | Backend 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 constts
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ốiRanh 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
- Chặn theo quyền — áp mô hình này vào route, menu, nút.
- Cấu hình IAM — lọc và bóc quyền từ response.
- Chạy dev không có IAM — bộ quyền giả khi phát triển.