Giao diện
Chặn theo quyền
Ba điểm chặn, khai báo độc lập nhau. Một màn hình thường dùng cả ba: trang cần quyền xem, mục menu ẩn theo cùng quyền đó, và nút hành động theo quyền sửa/xoá.
1. Truy cập trang
ts
// Trang công khai — bỏ qua cả kiểm tra đăng nhập
definePageMeta({ auth: false })
// Cần đăng nhập (mặc định, không cần khai)
definePageMeta({ auth: true })
// Cần một quyền
definePageMeta({ permission: '/orders/search' })
// Cần TẤT CẢ các quyền
definePageMeta({ permissions: ['/orders/search', '/orders/export'] })Hai middleware chạy theo thứ tự:
auth.global— chưa đăng nhập và route không khaiauth: false→ chuyển tới/auth/login?redirect=<đường dẫn hiện tại>.permission.global— thiếu quyền → ném lỗi 403, người dùng thấy trang lỗi của app.
Cần "chỉ cần một trong các quyền" thì middleware chung không làm được (nó luôn yêu cầu tất cả); tự kiểm trong trang:
ts
const { canAny } = usePermission()
if (!canAny(['/orders/search', '/orders/view-own'])) {
throw createError({ statusCode: 403, statusMessage: 'Bạn không có quyền xem đơn hàng.' })
}2. Mục menu
Khai permission trên NavItem; layout tự lọc trước khi hiển thị:
ts
// app/app.config.ts
export default defineAppConfig({
tasco: {
nav: [
{ label: 'Đơn hàng', path: '/orders', permission: '/orders/search' },
{
label: 'Quản trị',
permission: '/admin/access',
children: [
{ label: 'Người dùng', path: '/admin/users', permission: '/admin/users/search' },
],
},
],
},
})Lọc theo chiều sâu: nhóm cha bị ẩn thì cả nhánh biến mất; nhóm cha không khai permission vẫn hiện với những mục con mà người dùng được phép thấy.
Danh sách link ở nơi khác (tab, thẻ điều hướng nhanh) lọc bằng cùng một hàm:
ts
const { filterByPermission } = usePermission()
const tabs = computed(() => filterByPermission(ALL_TABS))3. Nút và phần tử trong trang
Directive v-can
vue
<!-- Không có quyền → ẩn hẳn -->
<CButton v-can="'/orders/create'" variant="primary">Tạo đơn</CButton>
<!-- Không có quyền → vẫn hiện nhưng khoá lại -->
<CButton v-can:disable="'/orders/delete'" danger>Xoá</CButton>
<!-- Cần TẤT CẢ các quyền -->
<CButton v-can="['/orders/update', '/orders/approve']">Duyệt</CButton>v-can:disable gắn thuộc tính disabled, thêm class is-disabled, chặn con trỏ và giảm độ mờ — dùng khi muốn người dùng biết chức năng tồn tại nhưng chưa được cấp quyền.
Component <Can>
Dùng khi cần bọc nhiều phần tử, cần "chỉ cần một quyền", hoặc cần nội dung thay thế:
vue
<Can permission="/orders/update">
<CButton @click="edit">Sửa</CButton>
<CButton danger @click="remove">Xoá</CButton>
</Can>
<Can :permissions="['/orders/approve', '/orders/reject']" any>
<CCard title="Phê duyệt">…</CCard>
<template #fallback>
<CEmpty description="Bạn không có quyền phê duyệt đơn này." />
</template>
</Can>v-can | <Can> | |
|---|---|---|
| Ẩn / khoá | Cả hai (v-can và v-can:disable) | Chỉ ẩn |
| "Chỉ cần một quyền" | Không | Có, qua any |
| Nội dung thay thế | Không | Có, qua slot #fallback |
| Áp cho một phần tử | Có | Bọc được nhiều phần tử |
Kiểm tra trong script
ts
const { can } = usePermission()
async function remove(id: string) {
if (!can('/orders/delete')) return
// ...
}Chọn cách nào
| Tình huống | Cách |
|---|---|
| Cả trang không được xem | definePageMeta({ permissions }) |
| Mục menu không nên hiện | permission trong NavItem |
| Nút hành động không được phép | v-can (ẩn) hoặc v-can:disable (khoá) |
| Cả khối nội dung, cần lời giải thích | <Can> + #fallback |
| Quyết định trong logic | usePermission().can() |
Nguyên tắc: ẩn thứ người dùng không bao giờ được dùng, khoá thứ họ có thể được cấp quyền sau này. Đừng để nút hiện bình thường rồi mới báo lỗi sau khi bấm.
Bẫy thường gặp
- Khai quyền ở trang nhưng quên ở menu — người dùng thấy mục menu, bấm vào thì gặp 403.
- Gõ sai URI: quyền so khớp chính xác,
/orders/searchschỉ đơn giản là không có ai có. - Trang đăng nhập quên
auth: false— middleware đẩy vòng lại chính nó. - Tin rằng ẩn nút là đã an toàn — backend vẫn phải kiểm tra.
Liên quan
- Mô hình phân quyền — URI, super-role,
usePermission. - Chạy dev không có IAM — bộ quyền giả để thử từng nhánh.
- Xử lý lỗi — trang 403 hiển thị thế nào.