Guard routing Vue 3 untuk akses berbasis peran: pola praktis
Guard routing Vue 3 untuk akses berbasis peran dijelaskan dengan pola praktis: aturan route meta, redirect aman, fallback 401/403 ramah, dan cara menghindari kebocoran data.

Apa yang sebenarnya diselesaikan route guards (dan yang tidak)\n\nRoute guards melakukan satu pekerjaan dengan baik: mereka mengontrol navigasi. Mereka memutuskan apakah seseorang boleh masuk ke sebuah route, dan ke mana mengirim mereka jika tidak bisa. Itu meningkatkan UX, tapi bukan hal yang sama dengan keamanan.\n\nMenyembunyikan item menu hanya sebuah indikasi, bukan otorisasi. Orang masih bisa mengetik URL, melakukan refresh pada deep link, atau membuka bookmark. Jika satu-satunya perlindungan Anda adalah “tombol tidak terlihat”, maka Anda tidak punya perlindungan.\n\nGuards berguna ketika Anda ingin aplikasi berperilaku konsisten sekaligus memblokir halaman yang tidak boleh ditampilkan, seperti area admin, tools internal, atau portal pelanggan berbasis peran.\n\nGuards membantu Anda:\n\n- Memblokir halaman sebelum dirender\n- Mengarahkan ke login atau default yang aman\n- Menampilkan layar 401/403 yang jelas alih-alih tampilan rusak\n- Menghindari loop navigasi yang tidak disengaja\n\nYang tidak bisa dilakukan guard adalah melindungi data sendirian. Jika sebuah API mengembalikan data sensitif ke browser, pengguna masih bisa memanggil endpoint itu langsung (atau memeriksa respons di dev tools) meskipun halamannya diblokir. Otorisasi nyata harus juga terjadi di server.\n\nTarget yang baik adalah menutup kedua sisi: memblokir halaman dan memblokir data. Jika seorang agen support membuka route khusus admin, guard harus menghentikan navigasi dan menampilkan “Akses ditolak”. Terpisah, backend Anda harus menolak panggilan API khusus admin, sehingga data terbatas tidak pernah dikirimkan.\n\n## Pilih model roles dan permissions yang sederhana\n\nKontrol akses menjadi rumit ketika Anda memulai dengan daftar panjang role. Mulailah dengan set kecil yang benar-benar dipahami orang, lalu tambahkan permission yang lebih rinci hanya ketika Anda benar-benar merasa perlu.\n\nPembagian praktis adalah:\n\n- Role menjelaskan siapa seseorang di aplikasi Anda.\n- Permission menjelaskan apa yang mereka bisa lakukan.\n\nUntuk kebanyakan tools internal, tiga role sudah mencakup banyak kasus:\n\n- admin: mengelola pengguna dan pengaturan, melihat semua data\n- support: menangani catatan dan respon pelanggan, tapi bukan pengaturan sistem\n- viewer: akses hanya-baca ke layar yang disetujui\n\nPutuskan lebih awal dari mana role berasal. Klaim token (JWT) cepat untuk guard tetapi bisa kadaluwarsa sampai diperbarui. Mem-fetch profil pengguna saat aplikasi dimulai selalu up-to-date, tapi guard Anda harus menunggu sampai request itu selesai.\n\nJuga pisahkan tipe route Anda dengan jelas: route publik (terbuka untuk semua), route terautentikasi (memerlukan sesi), dan route terbatas (memerlukan role atau permission).\n\n## Menentukan aturan akses dengan route meta\n\nCara paling bersih untuk mengekspresikan akses adalah mendeklarasikannya di route itu sendiri. Vue Router memungkinkan Anda melampirkan objek meta ke setiap record route sehingga guard dapat membacanya nanti. Ini menjaga aturan dekat dengan halaman yang dilindungi.\n\nPilih bentuk meta yang sederhana dan konsisten di seluruh aplikasi.\n\n```js
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 },
},
]
\n\nUntuk nested route, putuskan bagaimana aturan digabungkan. Di kebanyakan aplikasi, children harus mewarisi requirement parent. Di guard Anda, periksa setiap matched route record (jangan hanya `to.meta`) agar aturan parent tidak terlewat.\n\nSatu detail yang menghemat waktu nanti: bedakan antara “bisa melihat” dan “bisa mengedit”. Sebuah route mungkin terlihat untuk support dan admin, tetapi aksi edit harus dinonaktifkan untuk support. Flag `readOnly: true` di `meta` bisa menggerakkan perilaku UI (menonaktifkan aksi, menyembunyikan tombol destruktif) tanpa berpura-pura itu adalah keamanan.\n\n## Siapkan state auth agar guard berperilaku andal\n\nSebagian besar bug guard berasal dari satu masalah: guard berjalan sebelum aplikasi mengetahui siapa pengguna.\n\nPerlakukan auth seperti mesin status kecil dan jadikan itu satu sumber kebenaran. Anda ingin tiga status jelas:\n\n- **unknown**: aplikasi baru dimulai, sesi belum diperiksa\n- **logged out**: pengecekan sesi selesai, tidak ada pengguna valid\n- **logged in**: pengguna dimuat, role/permission tersedia\n\nAturan: jangan pernah membaca role saat auth berstatus **unknown**. Itu yang menyebabkan tampilan singkat dari layar yang dilindungi atau pengalihan mengejutkan ke login.\n\n### Tentukan bagaimana refresh sesi bekerja\n\nPilih satu strategi refresh dan pertahankan agar dapat diprediksi (mis. baca token, panggil endpoint “who am I”, set user).\n\nPola stabil terlihat seperti ini:\n\n- Saat aplikasi dimuat, set auth ke **unknown** dan mulai satu permintaan refresh tunggal\n- Resolusi guard hanya setelah refresh selesai (atau timeout)\n- Cache user di memory, bukan di route meta\n- Jika gagal, set auth ke **logged out**\n- Ekspos sebuah promise `ready` (atau serupa) yang bisa ditunggu oleh guard\n\nSetelah ini terpasang, logika guard tetap sederhana: tunggu auth siap, lalu putuskan akses.\n\n## Langkah demi langkah: implementasi otorisasi di level route\n\nPendekatan bersih adalah menjaga sebagian besar aturan di satu guard global, dan gunakan per-route guard hanya bila sebuah route benar-benar butuh logika khusus.\n\n### 1) Tambahkan global `beforeEach` guard\n\njs
// 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
})
\n\nIni meng-cover sebagian besar kasus tanpa menyebarkan pengecekan ke banyak komponen.\n\n### Ketika `beforeEnter` lebih tepat\n\nGunakan `beforeEnter` ketika aturan benar-benar spesifik route, seperti “hanya pemilik tiket yang bisa membuka halaman ini” dan bergantung pada `to.params.id`. Buat singkat dan gunakan kembali auth store yang sama agar perilaku konsisten.\n\n## Redirect aman tanpa membuka celah\n\nRedirect bisa diam-diam menggagalkan kontrol akses jika diperlakukan sebagai tepercaya.\n\nPolanya umum adalah: ketika pengguna belum login, kirim mereka ke login dan sertakan query param `returnTo`. Setelah login, baca dan navigasikan ke sana. Risikonya adalah open redirect (mengirim pengguna ke tempat yang tidak diinginkan) dan loop.\n\nJaga perilaku tetap sederhana:\n\n- Pengguna yang belum login pergi ke `Login` dengan `returnTo` di-set ke path saat ini.\n- Pengguna yang sudah login tapi tidak berwenang pergi ke halaman `Forbidden` khusus (bukan `Login`).\n- Hanya izinkan nilai `returnTo` internal yang Anda kenali.\n- Tambahkan satu pemeriksaan loop agar Anda tidak pernah mengarahkan ke tempat yang sama.\n\njs
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' }
}
})
\n\n## Hindari bocornya data terbatas selama navigasi\n\nKebocoran termudah adalah memuat data sebelum Anda tahu pengguna diizinkan melihatnya.\n\nDi Vue, ini sering terjadi ketika sebuah halaman mengambil data di `setup()` dan router guard berjalan sesaat kemudian. Bahkan jika pengguna diarahkan ulang, respons mungkin masih masuk ke store bersama atau tampil sebentar.\n\nAturan yang lebih aman: otorisasi dulu, lalu muat data.\n\njs
// 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' }
}
})
```\n\nJuga waspadai request terlambat ketika navigasi berubah cepat. Batalkan request (mis. dengan AbortController) atau abaikan respons terlambat dengan memeriksa request id.\n\nCaching adalah jebakan umum lainnya. Jika Anda menyimpan “last loaded customer record” secara global, respons khusus admin dapat ditampilkan kemudian kepada non-admin yang mengunjungi shell layar yang sama. Key cache menurut user id dan role, dan bersihkan modul sensitif saat logout (atau saat role berubah).\n\nBeberapa kebiasaan mencegah sebagian besar kebocoran:\n\n- Jangan fetch data sensitif sampai otorisasi dikonfirmasi.\n- Kunci data cache menurut user dan role, atau simpan lokal di halaman.\n- Batalkan atau abaikan request yang sedang berjalan saat route berubah.\n\n## Fallback ramah: 401, 403, dan not found\n\nJalur “tidak” sama pentingnya dengan jalur “ya”. Halaman fallback yang baik menjaga pengguna tetap orientasi dan mengurangi permintaan dukungan.\n\n### 401: Login diperlukan (belum terautentikasi)\n\nGunakan 401 ketika pengguna belum masuk. Sampaikan pesan singkat: mereka perlu login untuk melanjutkan. Jika Anda mendukung kembali ke halaman asli setelah login, validasi path return agar tidak mengarah keluar dari aplikasi Anda.\n\n### 403: Akses ditolak (sudah terautentikasi, tapi tidak diizinkan)\n\nGunakan 403 ketika pengguna sudah masuk tetapi tidak memiliki izin. Jaga pesan netral dan hindari memberi petunjuk detail sensitif.\n\nHalaman 403 yang baik biasanya memiliki judul jelas ("Akses ditolak"), satu kalimat penjelasan, dan langkah aman berikutnya (kembali ke dashboard, hubungi admin, atau ganti akun jika didukung).\n\n### 404: Tidak ditemukan\n\nTangani 404 terpisah dari 401/403. Jika tidak, orang mengira mereka tidak punya izin padahal halaman memang tidak ada.\n\n## Kesalahan umum yang merusak kontrol akses\n\nSebagian besar bug kontrol akses adalah slip logika sederhana yang muncul sebagai loop redirect, kilasan halaman yang salah, atau pengguna yang terjebak.\n\nPenyebab umum:\n\n- Menganggap UI tersembunyi sebagai “keamanan”. Selalu terapkan role di router dan di API.\n- Membaca role dari state usang setelah logout/login.\n- Mengarahkan pengguna yang tidak berwenang ke route lain yang juga terlindungi (loop instan).\n- Mengabaikan momen “auth masih memuat” saat refresh.\n- Mencampur 401 dan 403, yang membingungkan pengguna.\n\nContoh realistis: seorang agen support logout dan seorang admin login di komputer bersama. Jika guard membaca role yang dicache sebelum sesi baru dikonfirmasi, Anda bisa memblokir admin secara keliru atau, lebih buruk, sesaat mengizinkan akses yang seharusnya tidak boleh.\n\n## Daftar periksa cepat sebelum rilis\n\nLakukan pemeriksaan singkat yang fokus pada momen di mana kontrol akses biasanya gagal: jaringan lambat, sesi kadaluwarsa, dan URL yang di-bookmark.\n\n- Setiap route terlindungi memiliki meta requirement eksplisit.\n- Guards menangani state loading auth tanpa menampilkan UI terlindungi.\n- Pengguna tidak berwenang mendarat di halaman 403 yang jelas (bukan bounce membingungkan ke home).\n- Setiap redirect “kembali ke” divalidasi dan tidak bisa membuat loop.\n- Panggilan API sensitif dijalankan hanya setelah otorisasi dikonfirmasi.\n\nLalu uji satu skenario end-to-end: buka URL terlindungi di tab baru saat belum login, login sebagai pengguna dasar, dan pastikan Anda mendarat di halaman yang dimaksud (jika diizinkan) atau 403 yang bersih dengan langkah selanjutnya.\n\n## Contoh: akses support vs admin di aplikasi web kecil\n\nBayangkan aplikasi helpdesk dengan dua role: support dan admin. Support bisa membaca dan membalas tiket. Admin bisa juga, plus mengelola billing dan pengaturan perusahaan.\n\n- /tickets/:id diizinkan untuk support dan admin\n- /settings/billing hanya untuk admin\n\nSekarang momen umum: seorang agen support membuka deep link ke /settings/billing dari bookmark lama. Guard harus memeriksa route meta sebelum halaman dimuat dan memblokir navigasi. Karena pengguna sudah login tapi tidak punya role, mereka harus mendarat di fallback aman (403).\n\nDua pesan yang penting:\n\n- Login required (401): “Silakan masuk untuk melanjutkan.”\n- Access denied (403): “Anda tidak memiliki akses ke Billing Settings.”\n\nYang tidak boleh terjadi: komponen billing dimount, atau data billing di-fetch, bahkan sebentar.\n\nPerubahan role di tengah sesi adalah edge case lain. Jika seseorang dipromosikan atau diturunkan, jangan mengandalkan menu. Periksa ulang role saat navigasi dan putuskan bagaimana menangani halaman aktif: refresh state auth saat profil berubah, atau deteksi perubahan role dan arahkan keluar dari halaman yang tidak lagi diizinkan.\n\n## Langkah selanjutnya: jaga aturan akses tetap terawat\n\nSetelah guards bekerja, risiko lebih besar adalah drift: route baru dirilis tanpa meta, role berganti nama, dan aturan jadi tidak konsisten.\n\nUbah aturan Anda menjadi rencana uji kecil yang bisa dijalankan setiap kali Anda menambah route:\n\n- Sebagai Guest: buka route terlindungi dan konfirmasi Anda mendarat di halaman login tanpa melihat konten parsial.\n- Sebagai User: buka halaman yang seharusnya tidak bisa diakses dan konfirmasi Anda mendapat 403 yang jelas.\n- Sebagai Admin: coba deep link yang disalin dari address bar.\n- Untuk setiap role: refresh pada route terlindungi dan pastikan hasilnya stabil.\n\nJika Anda ingin satu lapisan keamanan tambahan, tambahkan view dev-only atau output console yang mencantumkan route dan requirement meta-nya, sehingga aturan yang hilang terlihat segera.\n\nJika Anda membangun tools internal atau portal dengan AppMaster (appmaster.io), Anda bisa menerapkan pendekatan yang sama: fokuskan route guards pada navigasi di UI Vue3, dan tegakkan permission di tempat logic dan data berada, yaitu backend.\n\nPilih satu perbaikan dan implementasikan secara end-to-end: ketatkan gating fetch data, perbaiki halaman 403, atau kunci penanganan redirect. Perbaikan kecil itulah yang menghentikan sebagian besar bug akses di dunia nyata.
FAQ
Route guards mengontrol navigasi, bukan akses data. Mereka membantu memblokir halaman, mengarahkan ulang, dan menampilkan status 401/403 yang jelas, tetapi mereka tidak bisa menghentikan seseorang memanggil API Anda langsung. Selalu terapkan izin yang sama di backend agar data terbatas tidak pernah dikirimkan.
Karena menyembunyikan UI hanya mengubah apa yang seseorang lihat, bukan apa yang bisa mereka minta. Pengguna masih bisa mengetik URL, membuka bookmark, atau menavigasi deep link. Anda perlu pemeriksaan router untuk memblokir halaman, dan otorisasi sisi server untuk memblokir data.
Mulailah dengan set kecil yang mudah dipahami, lalu tambahkan permission ketika benar-benar diperlukan. Baseline umum adalah admin, support, dan viewer, lalu tambahkan permission seperti tickets:read atau audit:read untuk tindakan spesifik. Pisahkan antara “siapa Anda” (role) dan “apa yang bisa Anda lakukan” (permission).
Taruh aturan akses di meta pada record route, seperti requiresAuth, roles, dan permissions. Ini menempatkan aturan dekat dengan halaman yang dilindungi dan membuat guard global Anda jadi bisa diprediksi. Untuk nested route, periksa semua record yang cocok sehingga requirement parent tidak terlewati.
Baca dari to.matched dan gabungkan requirement dari semua matched route record. Dengan begitu child route tidak bisa melewati requiresAuth atau roles milik parent. Putuskan aturan merge di awal (biasanya: requirement parent berlaku untuk children).
Karena guard bisa berjalan sebelum aplikasi mengetahui siapa pengguna. Perlakukan auth sebagai tiga state—unknown, logged out, logged in—dan jangan pernah mengevaluasi peran saat auth berstatus unknown. Buat guard menunggu inisialisasi tunggal (mis. satu panggilan "who am I") sebelum mengambil keputusan.
Gunakan global beforeEach untuk aturan konsisten seperti “memerlukan login” dan “memerlukan role/permission”. Gunakan beforeEnter hanya jika aturan benar-benar spesifik route dan bergantung pada params (mis. “hanya pemilik tiket yang bisa membuka halaman ini”). Pastikan kedua jalur menggunakan sumber kebenaran auth yang sama.
Anggap returnTo sebagai input yang tidak tepercaya. Hanya izinkan path internal yang Anda kenali (mis. nilai yang dimulai dengan / dan cocok dengan prefix yang dikenal), dan tambahkan pemeriksaan loop sehingga Anda tidak mengarahkan kembali ke route yang sama yang terblokir. Pengguna yang belum login pergi ke Login; pengguna yang sudah login tapi tidak berwenang pergi ke halaman 403 khusus.
Otorisasi dulu, baru fetch. Jika sebuah halaman mengambil data di setup() dan Anda mengarahkan ulang beberapa saat kemudian, respons bisa tetap masuk ke store atau tampil sebentar. Gating untuk request sensitif harus menunggu otorisasi terkonfirmasi, dan batalkan atau abaikan request yang sedang berjalan saat navigasi berubah.
Gunakan 401 saat pengguna belum masuk, dan 403 saat mereka sudah masuk tetapi tidak diizinkan. Jaga 404 terpisah sehingga pengguna tidak mengira mereka tidak memiliki izin untuk route yang sebenarnya tidak ada. Fallback yang jelas dan konsisten mengurangi kebingungan dan tiket dukungan.


