Vue 3 routing guards voor rolgebaseerde toegang: praktische patronen
Vue 3 routing guards voor rolgebaseerde toegang uitgelegd met praktische patronen: route-meta regels, veilige redirects, vriendelijke 401/403-fallbacks en het voorkomen van datalekken.

Wat route guards eigenlijk oplossen (en wat niet)
Route guards doen één ding goed: ze regelen navigatie. Ze beslissen of iemand een route mag betreden, en waar je heen stuurt als dat niet mag. Dat verbetert de UX, maar het is niet hetzelfde als beveiliging.
Het verbergen van een menu-item is slechts een aanwijzing, geen autorisatie. Mensen kunnen nog steeds een URL typen, vernieuwen op een deep link of een bookmark openen. Als je enige bescherming is “de knop is niet zichtbaar”, heb je geen bescherming.
Guards komen het meest tot hun recht als je wilt dat de app zich consistent gedraagt terwijl je pagina's blokkeert die niet getoond moeten worden, zoals admin-gebieden, interne tools of rolgebaseerde klantportalen.
Guards helpen je:
- Pagina's blokkeren voordat ze renderen
- Redirecten naar login of een veilige standaard
- Een duidelijke 401/403 tonen in plaats van een gebroken view
- Per ongeluk navigatielussen vermijden
Wat guards niet kunnen doen, is data op zichzelf beschermen. Als een API gevoelige data naar de browser retourneert, kan een gebruiker dat endpoint nog steeds direct aanroepen (of responses inspecteren in devtools) ook al is de pagina geblokkeerd. Echte autorisatie moet ook op de server gebeuren.
Een goed doel is om beide kanten te dekken: pagina's blokkeren en data blokkeren. Als een supportmedewerker een admin-only route opent, moet de guard de navigatie stoppen en “Toegang geweigerd” tonen. Separaat moet je backend admin-only API-aanroepen weigeren, zodat beperkte data nooit wordt teruggegeven.
Kies een simpel rollen- en permissiemodel
Toegangscontrole wordt ingewikkeld als je begint met een lange lijst rollen. Begin met een kleine set die mensen echt begrijpen, en voeg fijnmazigere permissies alleen toe als je er echt last van krijgt.
Een praktische verdeling is:
- Rollen beschrijven wie iemand is in je app.
- Permissies beschrijven wat ze kunnen doen.
Voor de meeste interne tools dekken drie rollen veel:
- admin: gebruikers en instellingen beheren, alle data zien
- support: klantgegevens en reacties afhandelen, maar geen systeeminstellingen
- viewer: alleen-lezen toegang tot goedgekeurde schermen
Bepaal vroeg waar rollen vandaan komen. Tokenclaims (JWT) zijn snel voor guards maar kunnen verouderd raken tot ze ververst worden. Het ophalen van het gebruikersprofiel bij het starten van de app is altijd actueel, maar je guards moeten wachten tot dat verzoek klaar is.
Scheid ook duidelijk je routetypen: publieke routes (voor iedereen open), geauthenticeerde routes (vereisen sessie) en beperkte routes (vereisen een rol of permissie).
Definieer toegangsregels met route meta
De schoonste manier om toegang uit te drukken is door het op de route zelf te declareren. Vue Router laat je een meta object aan elk route-record hangen zodat je guards het later kunnen lezen. Dit houdt regels dicht bij de pagina's die ze beschermen.
Kies een eenvoudige meta-structuur en houd je er in de hele app aan.
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 },
},
]
Voor geneste routes: beslis hoe regels worden gecombineerd. In de meeste apps zouden kinderen de oudervereisten moeten erven. Controleer in je guard elk gematcht route-record (niet alleen to.meta) zodat ouderregels niet worden overgeslagen.
Een detail dat later tijd bespaart: maak onderscheid tussen “kan bekijken” en “kan bewerken”. Een route kan zichtbaar zijn voor support en admins, maar bewerkingen moeten voor support uitgeschakeld zijn. Een readOnly: true-vlag in meta kan UI-gedrag aansturen (acties uitschakelen, destructieve knoppen verbergen) zonder te doen alsof het beveiliging is.
Bereid auth-state voor zodat guards betrouwbaar werken
De meeste guard-bugs komen door één probleem: de guard draait voordat de app weet wie de gebruiker is.
Behandel auth als een kleine toestandsmachine en maak het de enige bron van waarheid. Je wilt drie duidelijke staten:
- unknown: app is net gestart, sessie niet gecontroleerd
- logged out: sessiecheck afgerond, geen geldige gebruiker
- logged in: gebruiker geladen, rollen/permissies beschikbaar
De regel: lees nooit rollen terwijl auth unknown is. Dat is hoe je flashes van beschermde schermen of onverwachte redirects naar login krijgt.
Bepaal hoe sessieverversing werkt
Kies één refresh-strategie en houd het voorspelbaar (bijvoorbeeld: lees een token, roep een “who am I” endpoint aan, zet de user).
Een stabiel patroon ziet er zo uit:
- Bij app-load: zet auth op unknown en start één refresh-request
- Los guards pas op nadat refresh klaar is (of time-out)
- Cache de gebruiker in geheugen, niet in route
meta - Bij failure zet auth op logged out
- Bied een
readyPromise (of vergelijkbaar) aan die guards kunnen awaiten
Als dit staat blijft de guardlogica simpel: wacht tot auth klaar is en beslis dan over toegang.
Stap-voor-stap: route-level autorisatie implementeren
Een nette aanpak is om de meeste regels in één globale guard te houden en per-route guards alleen te gebruiken wanneer een route echt speciale logica nodig heeft.
1) Voeg een globale beforeEach guard toe
// 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
})
Dit dekt de meeste gevallen zonder checks door componenten te verspreiden.
Wanneer beforeEnter geschikter is
Gebruik beforeEnter wanneer de regel echt route-specifiek is, zoals “alleen de ticket-eigenaar mag deze pagina openen” en het afhangt van to.params.id. Houd het kort en hergebruik dezelfde auth-store zodat gedrag consistent blijft.
Veilige redirects zonder gaten te openen
Redirects kunnen stilletjes je toegangscontrole ongedaan maken als je ze als vertrouwd behandelt.
Het gebruikelijke patroon is: als een gebruiker is uitgelogd, stuur je naar login en voeg je een returnTo query-param toe. Na login lees je die en navigeer je erheen. Het risico is open redirects (gebruikers naar ongewenste plekken sturen) en lussen.
Houd het gedrag simpel:
- Uitgelogde gebruikers gaan naar
LoginmetreturnToingesteld op het huidige pad. - Ingelogde maar niet-geautoriseerde gebruikers gaan naar een speciale
Forbiddenpagina (nietLogin). - Sta alleen interne
returnTo-waardes toe die je herkent. - Voeg één luscheck toe zodat je nooit naar dezelfde plek redirect.
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' }
}
})
Voorkom dat gevoelige data lekt tijdens navigatie
De makkelijkste lek is data laden voordat je weet of de gebruiker het mag zien.
In Vue gebeurt dit vaak wanneer een pagina data ophaalt in setup() en de routerguard iets later draait. Zelfs als de gebruiker wordt doorgestuurd, kan de response toch in een gedeelde store belanden of kort zichtbaar zijn.
Een veiliger regel is: autoriseer eerst, laad daarna.
// 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' }
}
})
Let ook op late requests wanneer navigatie snel verandert. Annuleer requests (bijv. met AbortController) of negeer late responses door een request-id te controleren.
Caching is een andere veelvoorkomende valkuil. Als je een “laatst geladen klantrecord” globaal opslaat, kan een admin-only response later aan een niet-admin worden getoond die dezelfde schermshell bezoekt. Geef caches een sleutel per user-id en rol, en maak gevoelige modules leeg bij logout (of wanneer rollen veranderen).
Een paar gewoontes voorkomen de meeste lekken:
- Haal geen gevoelige data op totdat autorisatie is bevestigd.
- Geef gecachte data een sleutel per gebruiker en rol, of houd het lokaal op de pagina.
- Annuleer of negeer in-flight requests bij routewissel.
Vriendelijke fallbacks: 401, 403 en niet gevonden
De “nee”-paden zijn even belangrijk als de “ja”-paden. Goede fallback-pagina's houden gebruikers georiënteerd en verminderen supportverzoeken.
401: Login vereist (niet geauthenticeerd)
Gebruik 401 wanneer de gebruiker niet is ingelogd. Houd de boodschap zakelijk: ze moeten inloggen om door te gaan. Als je terugnavigatie ondersteunt na login, valideer het return-pad zodat het niet buiten je app kan wijzen.
403: Toegang geweigerd (geauthenticeerd, maar niet toegestaan)
Gebruik 403 wanneer de gebruiker is ingelogd maar niet de juiste permissie heeft. Houd het neutraal en voorkom het weggeven van gevoelige details.
Een degelijke 403-pagina heeft meestal een duidelijke titel (“Access denied”), één zin uitleg en een veilige volgende stap (terug naar dashboard, contact opnemen met een admin, wissel account als dat mogelijk is).
404: Niet gevonden
Behandel 404 apart van 401/403. Anders denken mensen dat ze geen permissie hebben terwijl de pagina gewoon niet bestaat.
Veelvoorkomende fouten die toegangscontrole breken
De meeste bugs in toegangscontrole zijn simpele logische fouten die verschijnen als redirect-lussen, het tonen van het verkeerde scherm of vastzittende gebruikers.
De gebruikelijke boosdoeners:
- Denken dat verborgen UI gelijk staat aan “beveiliging”. Handhaaf altijd rollen in de router en op de API.
- Rollen lezen uit verouderde state na logout/login.
- Niet-geautoriseerde gebruikers redirecten naar een andere beschermde route (onmiddellijke lus).
- Het “auth is nog aan het laden”-moment negeren bij refresh.
- 401 en 403 door elkaar halen, wat gebruikers verwart.
Een realistisch voorbeeld: een supportmedewerker logt uit en een admin logt in op dezelfde gedeelde computer. Als je guard een gecachte rol leest voordat de nieuwe sessie is bevestigd, kun je de admin onterecht blokkeren of, erger, kort toegang verlenen die niet zou moeten.
Snel checklist voordat je live gaat
Doe een korte controle die focust op de momenten waarop toegangscontrole meestal faalt: trage netwerken, verlopen sessies en gebookmarkte URL's.
- Elke beschermde route heeft expliciete
meta-vereisten. - Guards handelen de auth-loading state af zonder beschermde UI te tonen.
- Niet-geautoriseerde gebruikers komen op een duidelijke 403-pagina (niet een verwarrende bounce naar home).
- Elke “return to” redirect is gevalideerd en kan geen lussen maken.
- Gevoelige API-calls draaien pas nadat autorisatie bevestigd is.
Test daarna één scenario end-to-end: open een beschermde URL in een nieuw tabblad terwijl je uitgelogd bent, log in als een basisgebruiker en controleer dat je ofwel op de bedoelde pagina terechtkomt (als toegestaan) of op een nette 403 met een volgende stap.
Voorbeeld: support vs admin toegang in een kleine webapp
Stel je een helpdesk-app voor met twee rollen: support en admin. Support kan tickets lezen en beantwoorden. Admin kan dat ook, plus facturering en bedrijfsinstellingen beheren.
/tickets/:idis toegestaan voorsupportenadmin/settings/billingis alleen toegestaan vooradmin
Nu een veelvoorkomend moment: een supportmedewerker opent een deep link naar /settings/billing vanuit een oude bookmark. De guard moet de route-meta controleren vóórdat de pagina laadt en de navigatie blokkeren. Omdat de gebruiker is ingelogd maar niet de juiste rol heeft, moet hij op een veilige fallback (403) terechtkomen.
Twee boodschappen zijn belangrijk:
- Login vereist (401): “Log in om verder te gaan.”
- Toegang geweigerd (403): “Je hebt geen toegang tot Billing Settings.”
Wat er niet mag gebeuren: de billing-component mounten of billing-data kort ophalen.
Rolwijzigingen tijdens een sessie zijn een andere edge-case. Als iemand wordt gepromoveerd of gedegradeerd, vertrouw dan niet op het menu. Controleer rollen opnieuw bij navigatie en bedenk hoe je actieve pagina's afhandelt: ververs auth-state wanneer het profiel verandert, of detecteer rolveranderingen en redirect weg van pagina's die niet langer toegestaan zijn.
Volgende stappen: houd toegangsregels onderhoudbaar
Als de guards eenmaal werken, is het grotere risico drift: een nieuwe route wordt uitgebracht zonder meta, een rol wordt hernoemd en regels raken inconsistent.
Maak van je regels een klein testplan dat je kunt uitvoeren wanneer je een route toevoegt:
- Als Gast: open beschermde routes en controleer dat je op login belandt zonder gedeeltelijke content te zien.
- Als Gebruiker: open een pagina die je niet mag en controleer dat je een duidelijke 403 krijgt.
- Als Admin: probeer deep links gekopieerd uit de adresbalk.
- Voor elke rol: refresh op een beschermde route en controleer dat het resultaat stabiel is.
Als je een extra vangnet wilt, maak dan een dev-only view of console-output die routes en hun meta-vereisten toont, zodat missende regels direct opvallen.
Als je interne tools of portalen bouwt met AppMaster (appmaster.io), kun je dezelfde aanpak toepassen: houd route guards gefocust op navigatie in de Vue3 UI en handhaaf permissies waar de backend-logica en data leven.
Kies één verbetering en implementeer die end-to-end: verscherp de gating van data-fetches, verbeter de 403-pagina of sluit redirect-handling af. De kleine fixes zijn degene die de meeste echte toegangsbugs stoppen.
FAQ
Route guards regelen navigatie, niet data-toegang. Ze helpen een pagina te blokkeren, te redirecten en een nette 401/403-status te tonen, maar ze kunnen niet voorkomen dat iemand rechtstreeks je API aanroept. Handhaaf dezelfde permissies ook op de backend zodat beperkte data nooit wordt teruggestuurd.
Het verbergen van UI verandert alleen wat iemand ziet, niet wat hij kan opvragen. Gebruikers kunnen nog steeds een URL typen, bookmarks openen of deep links gebruiken. Je hebt routerchecks nodig om de pagina te blokkeren en server-side autorisatie om de data te blokkeren.
Begin met een kleine set die mensen begrijpen en voeg permissies toe wanneer je echte pijnpunten krijgt. Een gebruikelijke basis is admin, support en viewer, en voeg daarna permissies toe zoals tickets:read of audit:read voor specifieke acties. Houd ‘wie je bent’ (rol) gescheiden van ‘wat je kunt doen’ (permissie).
Plaats toegangsregels in meta op de route-records, zoals requiresAuth, roles en permissions. Dit houdt regels dicht bij de pagina's die ze beschermen en maakt je globale guard voorspelbaar. Bij geneste routes controleer je elk gematcht record zodat oudervereisten niet worden overgeslagen.
Lees uit to.matched en combineer de vereisten over alle gematchte route-records. Zo kan een child-route niet per ongeluk de requiresAuth of roles van een parent omzeilen. Spreek van tevoren af hoe je regels samenvoegt (meestal: oudervereisten gelden ook voor children).
De guard kan lopen voordat de app weet wie de gebruiker is. Behandel auth als drie staten—unknown, logged out, logged in—en evalueer rollen nooit terwijl auth unknown is. Laat guards wachten op één initialisatiestap (zoals een “who am I” request) voordat ze beslissen.
Gebruik standaard een globale beforeEach voor consistente regels zoals “vereist login” en “vereist rol/permissie.” Gebruik beforeEnter alleen wanneer de regel echt specifiek is voor die route en afhangt van params (bijv. “alleen de ticket-eigenaar kan deze pagina openen”). Zorg dat beide paden dezelfde auth-bron gebruiken.
Behandel returnTo als onbetrouwbare input. Sta alleen interne paden toe die je herkent (bijvoorbeeld waarden die met / beginnen en bekende prefixes matchen) en voeg een luscheck toe zodat je niet terugredirigeert naar dezelfde geblokkeerde route. Uitgelogde gebruikers gaan naar Login; ingelogde maar niet-geautoriseerde gebruikers gaan naar een aparte 403-pagina.
Autoriseren voordat je fetches doet. Als een pagina data laadt in setup() en je redirect kort daarna, kan de response toch in een store belanden of kort zichtbaar zijn. Gate gevoelige verzoeken achter bevestigde autorisatie, en annuleer of negeer lopende verzoeken bij routewissel.
Gebruik 401 als de gebruiker niet is ingelogd, en 403 als hij wel is ingelogd maar geen toestemming heeft. Houd 404 apart zodat gebruikers niet denken dat ze geen permissie hebben voor een route die gewoon niet bestaat. Duidelijke, consistente fallbacks verminderen verwarring en supporttickets.


