Vue 3 Routing-Guards für rollenbasierten Zugriff: praktische Muster
Vue 3 Routing-Guards für rollenbasierten Zugriff erklärt mit praktischen Mustern: Route-meta-Regeln, sichere Redirects, freundliche 401/403-Fallbacks und wie man Datenlecks vermeidet.

Was Route Guards wirklich lösen (und was nicht)
Route Guards haben eine klare Aufgabe: Sie kontrollieren die Navigation. Sie entscheiden, ob jemand eine Route betreten darf und wohin ihn die App schickt, wenn nicht. Das verbessert die UX, ist aber nicht dasselbe wie Sicherheit.
Ein Menüeintrag auszublenden ist nur ein Hinweis, keine Autorisierung. Leute können trotzdem eine URL eintippen, auf einen Deep Link aktualisieren oder ein Lesezeichen öffnen. Wenn dein einziger Schutz „der Button ist nicht sichtbar“ ist, hast du keinen Schutz.
Guards sind dann nützlich, wenn die App sich konsistent verhalten soll und Seiten blockiert werden müssen, die nicht angezeigt werden dürfen — etwa Admin-Bereiche, interne Tools oder rollenbasierte Kundenportale.
Guards helfen dir dabei:
- Seiten zu blockieren, bevor sie gerendert werden
- Zum Login oder einem sicheren Standard umzuleiten
- Eine klare 401/403-Seite statt einer kaputten Ansicht zu zeigen
- Unbeabsichtigte Navigationsschleifen zu vermeiden
Was Guards nicht leisten können, ist allein Daten zu schützen. Wenn eine API sensible Daten an den Browser zurückgibt, kann ein Nutzer diesen Endpunkt direkt aufrufen (oder Antworten in den DevTools inspizieren), auch wenn die Seite blockiert ist. Reale Autorisierung muss also auch auf dem Server stattfinden.
Ein gutes Ziel ist, beide Seiten abzudecken: Seiten blockieren und Daten schützen. Wenn ein Support-Mitarbeiter eine Admin-Route öffnet, sollte der Guard die Navigation stoppen und „Zugriff verweigert“ anzeigen. Separat sollte dein Backend Admin-exklusive API-Aufrufe ablehnen, sodass eingeschränkte Daten niemals zurückgegeben werden.
Wähle ein einfaches Rollen- und Berechtigungsmodell
Zugriffskontrolle wird kompliziert, wenn du mit einer langen Rollenliste startest. Beginne mit wenigen Rollen, die die Leute wirklich verstehen, und füge feinere Berechtigungen erst hinzu, wenn du echten Bedarf spürst.
Eine praktische Aufteilung ist:
- Rollen beschreiben, wer jemand in deiner App ist.
- Berechtigungen beschreiben, was sie tun dürfen.
Für die meisten internen Tools reichen drei Rollen weit:
- admin: verwaltet Nutzer und Einstellungen, sieht alle Daten
- support: bearbeitet Kundenfälle und Antworten, aber keine Systemeinstellungen
- viewer: Lesezugriff auf freigegebene Bereiche
Entscheide früh, woher die Rollen stammen. Token-Claims (JWT) sind schnell für Guards, können aber veraltet sein, bis sie aktualisiert werden. Das Nutzerprofil beim App-Start zu holen ist immer aktuell, aber deine Guards müssen warten, bis diese Anfrage fertig ist.
Trenne außerdem klar deine Routentypen: öffentliche Routen (für alle offen), authentifizierte Routen (Session erforderlich) und eingeschränkte Routen (bestimmte Rolle oder Permission nötig).
Definiere Regeln mit Route meta
Die sauberste Art, Zugriff auszudrücken, ist, ihn direkt an der Route zu deklarieren. Vue Router erlaubt es, jedem Route-Record ein meta-Objekt zu geben, das Guards später lesen können. So bleiben Regeln nahe an den Seiten, die sie schützen.
Wähle eine einfache meta-Form und bleibe in der gesamten App dabei.
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 },
},
]
Bei verschachtelten Routen entscheide, wie Anforderungen kombiniert werden. In den meisten Apps sollten Kinder die Anforderungen des Elternteils erben. Prüfe in deinem Guard alle matched-Route-Records (nicht nur to.meta), damit Elternregeln nicht übersprungen werden.
Ein Detail, das später Zeit spart: Unterscheide zwischen „ansehen dürfen“ und „bearbeiten dürfen“. Eine Route kann für Support und Admin sichtbar sein, aber Editieren sollte für Support deaktiviert werden. Ein readOnly: true-Flag in meta kann das UI-Verhalten steuern (Aktionen deaktivieren, destruktive Buttons ausblenden), ohne so zu tun, als wäre es Sicherheit.
Bereite den Auth-State vor, damit Guards zuverlässig arbeiten
Die meisten Guard-Bugs entstehen durch ein Problem: Der Guard läuft, bevor die App weiß, wer der Nutzer ist.
Behandle Auth wie eine kleine Zustandsmaschine und mach sie zur einzigen Wahrheit. Du willst drei klare Zustände:
- unknown: App gerade gestartet, Session noch nicht geprüft
- logged out: Session-Check fertig, kein gültiger Nutzer
- logged in: Nutzer geladen, Rollen/Berechtigungen verfügbar
Die Regel: Lese Rollen niemals, während Auth unknown ist. So vermeidest du das Aufblitzen geschützter Bildschirme oder überraschende Redirects zum Login.
Entscheide, wie Session-Refresh funktioniert
Wähle eine Refresh-Strategie und halte sie vorhersehbar (z. B.: Token lesen, einen „who am I“-Endpoint aufrufen, Nutzer setzen).
Ein stabiles Muster sieht so aus:
- Beim App-Start Auth auf unknown setzen und eine einzige Refresh-Anfrage starten
- Guards erst auflösen, wenn der Refresh fertig ist (oder ein Timeout tritt)
- Den Nutzer im Arbeitsspeicher cachen, nicht in Route
meta - Bei Fehlschlag Auth auf logged out setzen
- Ein
ready-Promise (oder Ähnliches) bereitstellen, das Guards abwarten können
Sobald das steht, bleibt die Guard-Logik einfach: Auf Auth warten, dann Zugriff entscheiden.
Schritt für Schritt: Route-Level-Autorisierung implementieren
Ein sauberer Ansatz ist, die meisten Regeln in einem globalen Guard zu halten und per-Route-Guards nur zu nutzen, wenn eine Route wirklich spezielle Logik braucht.
1) Füge einen globalen beforeEach-Guard hinzu
// 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
})
Das deckt die meisten Fälle ab, ohne Checks über Komponenten zu verteilen.
Wann beforeEnter besser passt
Verwende beforeEnter, wenn die Regel wirklich routenspezifisch ist, z. B. „nur der Ticket-Besitzer darf diese Seite öffnen“ und die Prüfung von to.params.id abhängt. Halte die Logik kurz und nutze denselben Auth-Store, damit das Verhalten konsistent bleibt.
Sichere Redirects ohne Lücken
Redirects können deine Zugriffskontrolle aushebeln, wenn du ihnen blind vertraust.
Das übliche Muster ist: Wenn ein Nutzer abgemeldet ist, schicke ihn zum Login und füge returnTo als Query-Param hinzu. Nach dem Login liest du das und navigierst dorthin. Das Risiko sind Open-Redirects (Nutzer auf unerwünschte Ziele schicken) und Loops.
Halte das Verhalten einfach:
- Abgemeldete Nutzer gehen zum
LoginmitreturnTo= aktueller Pfad. - Angemeldete, aber nicht berechtigte Nutzer gehen zu einer dedizierten
Forbidden-Seite (nichtLogin). - Erlaube nur interne
returnTo-Werte, die du erkennst. - Füge eine Loop-Prüfung hinzu, damit du nie zur selben Stelle weiterleitest.
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' }
}
})
Vermeide das Leaken eingeschränkter Daten während der Navigation
Die einfachste Leckquelle ist, Daten zu laden, bevor du weißt, ob der Nutzer sie sehen darf.
In Vue passiert das oft, wenn eine Seite in setup() Daten lädt und der Router-Guard kurz danach läuft. Selbst wenn der Nutzer umgeleitet wird, kann die Antwort in einem gemeinsamen Store landen oder kurz angezeigt werden.
Eine sichere Regel: erst autorisieren, dann laden.
// 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' }
}
})
Achte auch auf späte Requests, wenn die Navigation schnell wechselt. Breche Anfragen ab (z. B. mit AbortController) oder ignoriere verspätete Antworten durch einen Request-ID-Check.
Caching ist eine weitere Falle. Wenn du z. B. „letzten geladenen Kunden-Datensatz“ global speicherst, kann eine Admin-Antwort später einem Nicht-Admin angezeigt werden, der dieselbe Seitenstruktur besucht. Schlüssle Caches nach Nutzer-ID und Rolle auf und lösche sensible Module beim Logout (oder wenn sich Rollen ändern).
Einige Gewohnheiten verhindern die meisten Leaks:
- Lade sensible Daten erst, wenn die Autorisierung bestätigt ist.
- Schlüssle gecachte Daten nach Nutzer und Rolle oder halte sie lokal zur Seite.
- Breche oder ignoriere laufende Requests bei Routenwechseln.
Freundliche Fallbacks: 401, 403 und Not Found
Die „Nein“-Wege sind genauso wichtig wie die „Ja“-Wege. Gute Fallback-Seiten halten Nutzer orientiert und reduzieren Supportanfragen.
401: Login erforderlich (nicht authentifiziert)
Benutze 401, wenn der Nutzer nicht angemeldet ist. Halte die Botschaft einfach: Er muss sich einloggen, um fortzufahren. Falls du Rückkehr nach der Anmeldung unterstützt, validiere den Rückkehrpfad, damit er nicht außerhalb deiner App landet.
403: Zugriff verweigert (angemeldet, aber nicht erlaubt)
Benutze 403, wenn der Nutzer zwar angemeldet ist, aber die Berechtigung fehlt. Bleibe neutral und vermeide Hinweise auf sensible Details.
Eine solide 403-Seite hat meist einen klaren Titel („Access denied“), einen Satz Erklärung und einen sicheren nächsten Schritt (Zurück zum Dashboard, Admin kontaktieren, Account wechseln falls unterstützt).
404: Nicht gefunden
Behandle 404 getrennt von 401/403. Sonst denken Leute, sie hätten keine Berechtigung, obwohl die Seite einfach nicht existiert.
Häufige Fehler, die Zugriffskontrolle kaputt machen
Die meisten Fehler in der Zugriffskontrolle sind einfache Logikfehler, die sich als Redirect-Loops, Aufblitzen der falschen Seite oder feststeckende Nutzer zeigen.
Die üblichen Ursachen:
- Versteckte UI als „Sicherheit“ behandeln. Setze Rollen immer im Router und in der API durch.
- Rollen aus veraltetem State nach Logout/Login lesen.
- Nicht autorisierte Nutzer zu einer weiteren geschützten Route umleiten (so entsteht sofort eine Schleife).
- Den Moment ignorieren, in dem Auth noch lädt beim Refresh.
- 401 und 403 verwechseln, was Nutzer verwirrt.
Ein realistisches Beispiel: Ein Support-Mitarbeiter loggt sich aus und ein Admin loggt sich am selben Rechner ein. Wenn dein Guard eine gecachte Rolle liest, bevor die neue Session bestätigt ist, kannst du fälschlich den Admin blockieren oder – noch schlimmer – kurz Zugriff erlauben, den du nicht erlauben solltest.
Kurze Checkliste vor dem Release
Mache einen kurzen Durchgang, der die Momente fokussiert, in denen Zugriffskontrolle meist versagt: langsame Netze, abgelaufene Sessions und gespeicherte URLs.
- Jede geschützte Route hat explizite
meta-Anforderungen. - Guards behandeln den Auth-Ladezustand ohne geschützte UI aufzublitzen.
- Nicht autorisierte Nutzer landen auf einer klaren 403-Seite (nicht auf einer verwirrenden Weiterleitung zur Startseite).
- Jede „return to“-Weiterleitung wird validiert und kann keine Loops erzeugen.
- Sensible API-Aufrufe laufen erst nach bestätigter Autorisierung.
Teste dann ein Szenario Ende-zu-Ende: Öffne eine geschützte URL in einem neuen Tab, während du abgemeldet bist, melde dich als Basis-Nutzer an und prüfe, ob du entweder auf der Zielseite landest (falls erlaubt) oder eine saubere 403 mit einem nächsten Schritt siehst.
Beispiel: Support vs. Admin in einer kleinen Web-App
Stell dir ein Helpdesk mit zwei Rollen vor: support und admin. Support kann Tickets lesen und beantworten. Admin kann das auch, und zusätzlich Billing und Company-Settings verwalten.
/tickets/:idist fürsupportundadminerlaubt/settings/billingist nur füradminerlaubt
Ein häufiger Moment: Ein Support-Mitarbeiter öffnet ein altes Lesezeichen zu /settings/billing. Der Guard sollte die meta prüfen, bevor die Seite lädt, und die Navigation blockieren. Weil der Nutzer angemeldet, aber nicht berechtigt ist, landet er auf einem sicheren Fallback (403).
Zwei Nachrichten sind wichtig:
- Login erforderlich (401): „Please sign in to continue.“
- Zugriff verweigert (403): „You do not have access to Billing Settings.“
Was nicht passieren darf: Die Billing-Komponente mountet oder Billing-Daten werden auch nur kurz geladen.
Änderungen der Rolle während einer Session sind ein weiterer Randfall. Wenn jemand befördert oder degradiert wird, verlasse dich nicht auf das Menü. Prüfe Rollen bei der Navigation erneut und entscheide, wie du aktive Seiten behandelst: Aktualisiere den Auth-State, wenn sich das Profil ändert, oder erkenne Rollenwechsel und leite von Seiten weg, die nicht mehr erlaubt sind.
Nächste Schritte: Zugriffregeln wartbar halten
Wenn die Guards funktionieren, ist das größere Risiko Drift: Eine neue Route kommt ohne meta, eine Rolle wird umbenannt und Regeln werden inkonsistent.
Mach aus deinen Regeln einen kleinen Testplan, den du beim Hinzufügen einer Route durchgehst:
- Als Gast: öffne geschützte Routen und bestätige, dass du zum Login landest, ohne Teile des Inhalts zu sehen.
- Als User: öffne eine Seite, auf die du keinen Zugriff haben solltest, und bestätige, dass du eine klare 403 siehst.
- Als Admin: teste Deep Links aus der Adresszeile.
- Für jede Rolle: aktualisiere auf einer geschützten Route und bestätige, dass das Ergebnis stabil ist.
Wenn du eine zusätzliche Sicherheit willst, füge eine dev-only Ansicht oder Konsolenausgabe hinzu, die Routen und deren meta-Anforderungen auflistet, sodass fehlende Regeln sofort auffallen.
Wenn du interne Tools oder Portale mit AppMaster (appmaster.io) baust, kannst du denselben Ansatz anwenden: Halte Route Guards auf die Navigation im Vue3-UI fokussiert und erzwinge Berechtigungen dort, wo Backend-Logik und Daten liegen.
Wähle eine Verbesserung und implementiere sie Ende-zu-Ende: engere Gating-Logik für Datenabrufe, eine bessere 403-Seite oder strengere Redirect-Verarbeitung. Kleine Fixes sind die, die die meisten realen Zugriffsbugs stoppen.
FAQ
Route Guards kontrollieren Navigation, nicht den Datenzugriff. Sie helfen dabei, eine Seite zu blockieren, umzuleiten und einen sauberen 401/403-Zustand anzuzeigen, können aber nicht verhindern, dass jemand direkt deine API aufruft. Setze dieselben Berechtigungen auch auf dem Backend durch, sodass eingeschränkte Daten niemals zurückgegeben werden.
Weil das Ausblenden einer UI nur ändert, was jemand sieht, nicht, was er anfragen kann. Nutzer können weiterhin eine URL eintippen, Lesezeichen öffnen oder Deep Links benutzen. Du brauchst Router-Checks, um die Seite zu blockieren, und serverseitige Autorisierung, um die Daten zu schützen.
Starte mit wenigen, leicht verständlichen Rollen und ergänze Berechtigungen nur bei Bedarf. Ein gängiges Basisset sind admin, support und viewer. Ergänze bei Bedarf spezifische Permissions wie tickets:read oder audit:read. Trenne dabei klar „wer du bist“ (Rolle) von „was du tun kannst“ (Permission).
Lege Zugriffsregeln in meta der Routen ab, z. B. requiresAuth, roles und permissions. So stehen die Regeln dicht bei den geschützten Seiten und dein globaler Guard bleibt vorhersehbar. Bei verschachtelten Routen prüfe alle matched-Records, damit Elterneinstellungen nicht umgangen werden.
Lese aus to.matched und kombiniere die Anforderungen aller übereinstimmenden Route-Records. So kann eine Kindroute nicht versehentlich eine Eltern-requiresAuth oder -roles umgehen. Lege vorher fest, wie die Anforderungen zusammengeführt werden (üblich: Elternanforderungen gelten für Kinder).
Oft läuft der Guard, bevor die App weiß, wer der Nutzer ist. Behandle Auth als drei Zustände—unknown, logged out, logged in—und werte Rollen niemals aus, während Auth unknown ist. Lass Guards auf eine Initialisierung (z. B. einen einmaligen „who am I“-Request) warten, bevor sie entscheiden.
Nutze einen globalen beforeEach für konsistente Regeln wie „Login erforderlich“ oder „Rolle/Berechtigung nötig“. Verwende beforeEnter nur, wenn die Regel wirklich routenspezifisch ist und von Parametern abhängt (z. B. „nur der Besitzer des Tickets darf diese Seite öffnen“). Beide Wege sollten dieselbe Auth-Quelle nutzen.
Behandle returnTo als untrusted input. Erlaube nur interne Pfade, die du erkennst (z. B. Werte, die mit / beginnen und bekannten Präfixen entsprechen), und baue eine Loop-Prüfung ein, damit du nicht zurück auf dieselbe gesperrte Route leitest. Abgemeldete Nutzer gehen zu Login; angemeldete, aber nicht berechtigte Nutzer zu einer dedizierten 403-Seite.
Autorisieren bevor du Daten lädst. Wenn eine Seite in setup() Daten anfordert und du kurz danach umleitest, kann die Antwort dennoch in einen Store landen oder kurz sichtbar werden. Schütze sensitive Requests hinter bestätigter Autorisierung und breche oder ignoriere laufende Requests bei Routenwechseln.
Verwende 401, wenn der Nutzer nicht angemeldet ist; 403, wenn der Nutzer zwar angemeldet ist, aber keine Berechtigung hat. Halte 404 separat, damit Nutzer nicht denken, sie hätten keine Berechtigung für eine Seite, die einfach nicht existiert. Klare, konsistente Fallbacks reduzieren Verwirrung und Supportanfragen.


