Guard định tuyến Vue 3 cho phân quyền theo vai trò: các mẫu thực tế
Guard định tuyến Vue 3 cho phân quyền theo vai trò, giải thích bằng các mẫu thực tế: meta route, redirect an toàn, fallback 401/403 thân thiện và cách tránh rò rỉ dữ liệu.

Guard định tuyến giải quyết điều gì (và không giải quyết gì)
Guard định tuyến làm tốt một việc: chúng kiểm soát điều hướng. Chúng quyết định ai có thể vào một route, và gửi họ tới đâu nếu không thể. Điều đó cải thiện UX, nhưng không phải là bảo mật.
Ẩn một mục menu chỉ là gợi ý, không phải ủy quyền. Người ta vẫn có thể gõ URL, làm mới một deep link, hoặc mở bookmark. Nếu biện pháp duy nhất của bạn là “nút không hiển thị”, thì bạn không có bảo vệ thực sự.
Guards phát huy khi bạn muốn app hành xử nhất quán trong khi chặn các trang không nên hiển thị, như khu admin, công cụ nội bộ, hoặc cổng khách theo vai trò.
Guards giúp bạn:
- Chặn trang trước khi render
- Chuyển hướng đến login hoặc mặc định an toàn
- Hiển thị màn 401/403 rõ ràng thay vì view lỗi
- Tránh vòng lặp điều hướng vô tình
Những gì guards không làm được là bảo vệ dữ liệu một mình. Nếu một API trả dữ liệu nhạy cảm về trình duyệt, người dùng vẫn có thể gọi endpoint đó trực tiếp (hoặc kiểm tra phản hồi trong dev tools) dù trang bị chặn. Ủy quyền thực sự phải diễn ra phía server.
Mục tiêu tốt là bao phủ cả hai phía: chặn trang và chặn dữ liệu. Nếu một nhân viên hỗ trợ mở route chỉ dành admin, guard nên dừng điều hướng và hiển thị “Access denied”. Riêng backend của bạn nên từ chối các cuộc gọi API chỉ dành admin, để dữ liệu giới hạn không bao giờ được trả về.
Chọn mô hình vai trò và quyền đơn giản
Kiểm soát truy cập trở nên rối khi bạn bắt đầu với danh sách vai trò dài. Bắt đầu với một tập nhỏ mà người dùng thực sự hiểu, rồi thêm quyền chi tiết khi thấy cần thiết.
Phân tách thực tế:
- Roles miêu tả người đó là ai trong app của bạn.
- Permissions miêu tả họ có thể làm gì.
Với hầu hết công cụ nội bộ, ba vai trò bao phủ nhiều trường hợp:
- admin: quản lý người dùng và cấu hình, xem mọi dữ liệu
- support: xử lý hồ sơ và phản hồi khách hàng, nhưng không thay đổi cài đặt hệ thống
- viewer: chỉ đọc các màn được duyệt
Quyết định sớm nơi vai trò đến từ đâu. Claims trong token (JWT) nhanh cho guards nhưng có thể trở nên lỗi thời cho đến khi refresh. Lấy profile người dùng khi app khởi động luôn luôn cập nhật, nhưng guard của bạn phải chờ request đó hoàn thành.
Cũng hãy tách rõ các loại route: public (mọi người truy cập), authenticated (cần session), và restricted (cần role hoặc permission).
Định nghĩa quy tắc truy cập bằng route meta
Cách gọn nhất để biểu đạt truy cập là khai báo trên chính route. Vue Router cho phép bạn gắn một đối tượng meta vào mỗi record route để guard sau này đọc. Điều này giữ quy tắc gần trang bạn bảo vệ.
Chọn một cấu trúc meta đơn giản và dùng nhất quán trong app.
const routes = [
{
path: "/admin",
component: () => import("@/pages/AdminLayout.vue"),
meta: { requiresAuth: true, roles: ["admin"] },
children: [
{
path: "users",
component: () => import("@/pages/AdminUsers.vue"),
// inherits requiresAuth + roles from parent
},
{
path: "audit",
component: () => import("@/pages/AdminAudit.vue"),
meta: { permissions: ["audit:read"] },
},
],
},
{
path: "/tickets",
component: () => import("@/pages/Tickets.vue"),
meta: { requiresAuth: true, permissions: ["tickets:read"], readOnly: true },
},
]
Với route lồng nhau, quyết định cách kết hợp quy tắc. Trong hầu hết app, child nên kế thừa yêu cầu của parent. Trong guard của bạn, kiểm tra mọi record khớp (không chỉ to.meta) để không bỏ qua quy tắc của parent.
Một chi tiết hay giúp tiết kiệm thời gian: phân biệt giữa “có thể xem” và “có thể chỉnh sửa”. Một route có thể hiển thị cho support và admin, nhưng hành động chỉnh sửa nên bị vô hiệu cho support. Cờ readOnly: true trong meta có thể điều khiển hành vi UI (vô hiệu hóa hành động, ẩn nút gây hại) mà không giả vờ đó là bảo mật.
Chuẩn bị trạng thái auth để guard hoạt động đáng tin cậy
Hầu hết lỗi guard xuất phát từ một vấn đề: guard chạy trước khi app biết người dùng là ai.
Xử lý auth như một máy trạng thái nhỏ và làm cho nó là nguồn chân lý duy nhất. Bạn muốn ba trạng thái rõ ràng:
- unknown: app vừa khởi động, session chưa được kiểm tra
- logged out: kiểm tra session xong, không có user hợp lệ
- logged in: user đã load, roles/permissions có sẵn
Quy tắc: không bao giờ đọc roles khi auth ở trạng thái unknown. Đó là cách bạn gặp hiện tượng chớp trang bảo vệ hoặc redirect bất ngờ tới login.
Quyết định cách refresh session
Chọn một chiến lược refresh và giữ nó có thể đoán (ví dụ: đọc token, gọi endpoint “who am I”, đặt user).
Một mẫu ổn định trông như sau:
- Khi app load, đặt auth là unknown và bắt một request refresh duy nhất
- Hoãn quyết định của guard cho tới khi refresh xong (hoặc hết thời gian)
- Lưu cache user trong bộ nhớ, không lưu trong route meta
- Nếu thất bại, đặt auth là logged out
- Phơi bày một promise
ready(hoặc tương tự) để guard có thể await
Khi đã có điều này, logic guard sẽ đơn giản: chờ auth sẵn sàng, rồi quyết định truy cập.
Từng bước: triển khai ủy quyền ở mức route
Cách gọn là giữ hầu hết quy tắc trong một guard toàn cục, và dùng guard per-route chỉ khi route thực sự cần logic đặc thù.
1) Thêm một beforeEach toàn cục
// router/index.js
router.beforeEach(async (to) => {
const auth = useAuthStore()
// Step 2: wait for auth initialization when needed
if (!auth.ready) await auth.init()
// Step 3: check authentication, then roles/permissions
if (to.meta.requiresAuth && !auth.isAuthenticated) {
return { name: 'login', query: { redirect: to.fullPath } }
}
const roles = to.meta.roles
if (roles && roles.length > 0 && !roles.includes(auth.userRole)) {
return { name: 'forbidden' } // 403
}
// Step 4: allow navigation
return true
})
Điều này bao phủ hầu hết trường hợp mà không phải rải kiểm tra trong components.
Khi beforeEnter phù hợp hơn
Dùng beforeEnter khi quy tắc thực sự là route-specific, như “chỉ chủ ticket mới mở được trang này” và phụ thuộc vào to.params.id. Giữ cho nó ngắn và tái sử dụng cùng auth store để hành vi nhất quán.
Redirect an toàn mà không mở lỗ hổng
Redirect có thể lặng lẽ phá vỡ kiểm soát truy cập nếu bạn coi chúng là đáng tin. Mẫu phổ biến: khi user chưa đăng nhập, gửi họ tới login và kèm returnTo. Sau login, đọc giá trị đó và điều hướng về. Rủi ro là open redirect (gửi người dùng ra ngoài) và vòng lặp.
Giữ hành vi đơn giản:
- Người chưa đăng nhập đi tới
LoginvớireturnTolà path hiện tại. - Người đã đăng nhập nhưng không được phép đi tới trang
Forbiddenriêng (không phảiLogin). - Chỉ cho phép giá trị
returnTonội bộ mà bạn nhận diện. - Thêm kiểm tra vòng lặp để không redirect về cùng một nơi.
const allowedReturnTo = (to) => {
if (!to || typeof to !== 'string') return null
if (!to.startsWith('/')) return null
// optional: only allow known prefixes
if (!['/app', '/admin', '/tickets'].some(p => to.startsWith(p))) return null
return to
}
router.beforeEach((to) => {
if (!auth.isReady) return false
if (!auth.isLoggedIn && to.name !== 'Login') {
return { name: 'Login', query: { returnTo: to.fullPath } }
}
if (auth.isLoggedIn && !canAccess(to, auth.user) && to.name !== 'Forbidden') {
return { name: 'Forbidden' }
}
})
Tránh rò rỉ dữ liệu bị hạn chế trong lúc điều hướng
Lỗ hổng dễ nhất là tải dữ liệu trước khi biết người dùng có quyền xem hay không.
Trong Vue, điều này thường xảy ra khi một trang fetch dữ liệu trong setup() và router guard chạy chậm hơn một chút. Ngay cả khi người dùng bị redirect, phản hồi vẫn có thể rơi vào store chung hoặc hiện thoáng trên màn hình.
Quy tắc an toàn hơn là: xác thực trước, rồi mới load.
// router guard: authorize before entering the route
router.beforeEach(async (to) => {
await auth.ready() // ensure roles are known
const required = to.meta.requiredRole
if (required && !auth.hasRole(required)) {
return { name: 'forbidden' }
}
})
Cũng chú ý các request muộn khi điều hướng thay đổi nhanh. Huỷ request (ví dụ với AbortController) hoặc bỏ qua phản hồi muộn bằng cách kiểm tra một request id.
Caching là một bẫy khác. Nếu bạn lưu “bản ghi khách hàng cuối cùng” toàn cục, một phản hồi chỉ dành admin có thể được hiển thị sau đó cho một non-admin khi họ vào cùng khung màn. Scope cache theo user id và role, và xóa module nhạy cảm khi logout (hoặc khi role thay đổi).
Một vài thói quen ngăn hầu hết rò rỉ:
- Không fetch dữ liệu nhạy cảm cho đến khi ủy quyền được xác nhận.
- Khóa cache theo người dùng và role, hoặc giữ dữ liệu cục bộ trong trang.
- Huỷ hoặc bỏ qua các request đang chạy khi route thay đổi.
Fallback thân thiện: 401, 403 và 404
"Không" cũng quan trọng như "có". Trang fallback tốt giúp người dùng định hướng và giảm yêu cầu hỗ trợ.
401: Cần đăng nhập (chưa xác thực)
Dùng 401 khi người dùng chưa đăng nhập. Thông điệp ngắn gọn: họ cần đăng nhập để tiếp tục. Nếu bạn hỗ trợ quay về trang ban đầu sau login, kiểm tra path trước để không cho phép trỏ ra ngoài app.
403: Từ chối truy cập (đã xác thực nhưng không được phép)
Dùng 403 khi người dùng đã đăng nhập nhưng thiếu quyền. Giữ thông điệp trung lập và tránh tiết lộ chi tiết nhạy cảm.
Một trang 403 tốt thường có tiêu đề rõ ràng ("Access denied"), một câu giải thích ngắn, và một bước an toàn tiếp theo (quay về dashboard, liên hệ admin, hoặc chuyển đổi tài khoản nếu hỗ trợ).
404: Không tìm thấy
Xử lý 404 riêng với 401/403. Nếu không, người dùng dễ nghĩ họ bị từ chối trong khi trang chỉ không tồn tại.
Sai lầm phổ biến làm hỏng kiểm soát truy cập
Hầu hết lỗi kiểm soát truy cập là những sai sót logic đơn giản xuất hiện dưới dạng vòng lặp redirect, chớp trang sai, hoặc người dùng bị kẹt.
Nguyên nhân thường gặp:
- Coi UI bị ẩn là “bảo mật”. Luôn thực thi role ở router và trên API.
- Đọc role từ trạng thái cũ sau logout/login.
- Redirect người không được phép tới một route khác cũng bị bảo vệ (vòng lặp ngay lập tức).
- Bỏ qua trạng thái “auth đang load” khi refresh.
- Nhầm lẫn giữa 401 và 403, gây nhầm lẫn cho người dùng.
Ví dụ thực tế: một nhân viên support logout và một admin login trên cùng máy. Nếu guard đọc role cache trước khi session mới được xác nhận, bạn có thể block admin sai, hoặc tệ hơn, cho phép truy cập tạm thời mà không nên.
Checklist nhanh trước khi phát hành
Làm một lượt ngắn tập trung vào các thời điểm thường gây lỗi truy cập: mạng chậm, session hết hạn, và URL bookmark.
- Mỗi route bảo vệ có
metarõ ràng. - Guards xử lý trạng thái auth-loading mà không làm chớp UI bảo vệ.
- Người không có quyền đến trang 403 rõ ràng (không bounce về home gây nhầm lẫn).
- Mọi redirect “quay về” được xác thực và không tạo vòng lặp.
- Các API nhạy cảm chỉ chạy sau khi ủy quyền được xác nhận.
Rồi kiểm thử một kịch bản end-to-end: mở một URL bảo vệ trong tab mới khi chưa đăng nhập, đăng nhập như user cơ bản, và xác nhận bạn hoặc tới trang dự định (nếu được phép) hoặc nhận một 403 rõ ràng với bước tiếp theo.
Ví dụ: phân quyền support vs admin trong một web app nhỏ
Giả sử một helpdesk với hai vai trò: support và admin. Support có thể đọc và trả lời ticket. Admin cũng làm được, thêm quản lý billing và cài đặt công ty.
/tickets/:idcho phépsupportvàadmin/settings/billingchỉ choadmin
Khoảnh khắc thường gặp: một agent support mở deep link tới /settings/billing từ bookmark cũ. Guard nên kiểm tra meta trước khi trang load và chặn điều hướng. Vì user đã đăng nhập nhưng thiếu role, họ nên tới fallback an toàn (403).
Hai thông điệp quan trọng:
- Login required (401): “Vui lòng đăng nhập để tiếp tục.”
- Access denied (403): “Bạn không có quyền truy cập Cài đặt Billing.”
Những gì không được xảy ra: component billing mount, hoặc dữ liệu billing được fetch, dù chỉ chớp mắt.
Thay đổi role giữa phiên là một edge case khác. Nếu ai đó được thăng quyền hoặc hạ quyền, đừng chỉ tin vào menu. Kiểm tra lại role khi điều hướng và quyết định xử lý các trang đang mở: refresh auth khi profile thay đổi, hoặc phát hiện thay đổi role và redirect khỏi các trang không còn hợp lệ.
Bước tiếp theo: giữ quy tắc truy cập dễ bảo trì
Khi guards đã hoạt động, rủi ro lớn hơn là sự trôi dạt: route mới được ship mà không có meta, role bị đổi tên, và quy tắc không nhất quán.
Biến quy tắc thành một kế hoạch test nhỏ bạn có thể chạy mỗi khi thêm route:
- Với Guest: mở route bảo vệ và xác nhận bạn tới login mà không thấy nội dung một phần.
- Với User: mở trang bạn không được phép và xác nhận nhận 403 rõ ràng.
- Với Admin: thử các deep link từ thanh địa chỉ.
- Với mỗi role: refresh trên route bảo vệ và xác nhận kết quả ổn định.
Nếu muốn một an toàn thêm, thêm một view chỉ dev hoặc in ra console liệt kê route và meta tương ứng, để các rule thiếu nổi bật ngay.
Nếu bạn đang xây dựng công cụ nội bộ hoặc cổng với AppMaster (appmaster.io), bạn có thể áp dụng cùng cách tiếp cận: giữ route guards tập trung vào điều hướng trong Vue 3 UI, và áp dụng quyền nơi logic backend và dữ liệu tồn tại.
Chọn một cải tiến và triển khai end to end: thắt chặt gating khi fetch dữ liệu, cải thiện trang 403, hoặc khoá xử lý redirect. Những sửa nhỏ thường là cách chặn hầu hết lỗi truy cập thực tế.
Câu hỏi thường gặp
Route guards kiểm soát điều hướng, không phải truy cập dữ liệu. Chúng giúp chặn trang, chuyển hướng và hiển thị trạng thái 401/403 rõ ràng, nhưng không thể ngăn người ta gọi API trực tiếp. Luôn áp dụng cùng quyền trên backend để dữ liệu bị giới hạn không bao giờ được trả về.
Vì việc ẩn UI chỉ thay đổi những gì người ta thấy, không thay đổi những gì họ có thể yêu cầu. Người dùng vẫn có thể gõ URL, mở bookmark hoặc deep link. Bạn cần kiểm tra ở router để chặn trang, và ủy quyền phía server để chặn dữ liệu.
Bắt đầu với một tập nhỏ mà mọi người hiểu, rồi thêm quyền chi tiết khi thực sự cần. Một bộ cơ bản thường là admin, support và viewer, rồi thêm quyền như tickets:read hoặc audit:read cho các hành động cụ thể. Giữ sự khác biệt giữa “bạn là ai” (role) và “bạn có thể làm gì” (permission).
Đặt quy tắc truy cập trong meta của record route, như requiresAuth, roles và permissions. Điều này giữ quy tắc gần với trang được bảo vệ và khiến guard toàn cục dễ dự đoán. Với route lồng nhau, kiểm tra mọi record trong to.matched để không bỏ sót yêu cầu của parent.
Đọc từ to.matched và kết hợp yêu cầu trên tất cả record khớp. Bằng cách đó, route con không thể vô tình vượt qua requiresAuth hoặc roles của parent. Quyết định cách kết hợp rõ ràng từ đầu (thường: yêu cầu của parent áp dụng cho children).
Vì guard có thể chạy trước khi app biết người dùng là ai. Xử lý auth như ba trạng thái—unknown, logged out, logged in—và tuyệt đối không đánh giá role khi auth ở trạng thái unknown. Làm cho guard chờ bước khởi tạo (ví dụ một gọi “who am I”) trước khi quyết định.
Mặc định dùng beforeEach toàn cục cho các quy tắc nhất quán như “cần đăng nhập” và “cần role/permission”. Dùng beforeEnter khi quy tắc thực sự phụ thuộc vào params của route, ví dụ “chỉ chủ ticket mới được mở trang này”. Cả hai cách nên dùng cùng nguồn auth làm chân lý duy nhất.
Xử lý returnTo như input không tin tưởng được. Chỉ cho phép các đường dẫn nội bộ bạn nhận diện (ví dụ bắt đầu bằng / và khớp các tiền tố đã biết), và thêm kiểm tra vòng lặp để không redirect về route bị chặn. Người chưa đăng nhập đến Login; người đã đăng nhập nhưng không có quyền đến trang 403 chuyên dụng.
Ủy quyền trước khi fetch. Nếu trang fetch dữ liệu trong setup() rồi bạn redirect ngay sau, phản hồi vẫn có thể rơi vào store hoặc hiện thoáng trên màn hình. Khóa các yêu cầu nhạy cảm sau khi ủy quyền được xác nhận, và huỷ hoặc bỏ qua các yêu cầu đang chạy khi chuyển route.
Dùng 401 khi người dùng chưa đăng nhập, và 403 khi họ đã đăng nhập nhưng không được phép. Giữ 404 riêng để người dùng không nghĩ họ bị từ chối khi trang chỉ đơn giản không tồn tại. Các trang fallback rõ ràng và nhất quán sẽ giảm nhầm lẫn và ticket hỗ trợ.


