30 thg 11, 2025·6 phút đọc

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 Vue 3 cho phân quyền theo vai trò: các mẫu thực tế

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

Triển khai lên cloud của bạn
Phát hành lên AppMaster Cloud, AWS, Azure, hoặc Google Cloud khi bạn sẵn sàng.
Triển khai App

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

Ngăn rò rỉ dữ liệu
Khoá các yêu cầu nhạy cảm phía backend bằng những kiểm tra bạn định nghĩa trực quan trong AppMaster.
Bảo vệ dữ liệu

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 Login với returnTo là path hiện tại.
  • Người đã đăng nhập nhưng không được phép đi tới trang Forbidden riêng (không phải Login).
  • Chỉ cho phép giá trị returnTo nộ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

Phát hành công cụ nội bộ
Tạo dashboard admin và công cụ hỗ trợ với kiểm soát truy cập trong AppMaster.
Xây dựng Tool

"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

Kết nối dịch vụ quan trọng
Thêm thanh toán Stripe và thông báo Telegram hoặc email bằng tích hợp sẵn.
Thêm tích hợp

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ó meta rõ 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ỏ

Thêm ủy quyền thực sự phía backend
Thiết kế API và quy tắc nghiệp vụ trong AppMaster để dữ liệu được bảo vệ, không chỉ các route.
Tạo Backend

Giả sử một helpdesk với hai vai trò: supportadmin. 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/:id cho phép supportadmin
  • /settings/billing chỉ cho admin

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 thực sự là bảo mật hay chỉ là UX?

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ề.

Tại sao ẩn một mục menu không đủ cho phân quyền theo vai trò?

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.

Mô hình vai trò và quyền đơn giản để bắt đầu là gì?

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, supportviewer, 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).

Nên dùng Vue Router `meta` như thế nào cho kiểm soát truy cập?

Đặt quy tắc truy cập trong meta của record route, như requiresAuth, rolespermissions. Đ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.

Làm sao xử lý route lồng nhau để các child kế thừa hạn chế 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).

Làm sao ngăn vòng lặp redirect và việc hiển thị chớp trang được bảo vệ khi refresh?

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.

Khi nào nên dùng global `beforeEach` vs `beforeEnter`?

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.

Làm sao thực hiện redirect sau khi đăng nhập mà không tạo lỗ hổng open redirect?

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.

Làm sao tránh rò rỉ dữ liệu bị hạn chế trong lúc điều hướ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.

Cách dùng 401 vs 403 vs 404 đúng trong app Vue là gì?

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ợ.

Dễ dàng bắt đầu
Tạo thứ gì đó tuyệt vời

Thử nghiệm với AppMaster với gói miễn phí.
Khi bạn sẵn sàng, bạn có thể chọn đăng ký phù hợp.

Bắt đầu