Strażnicy routingu w Vue 3 dla dostępu opartego na rolach: praktyczne wzorce
Strażnicy routingu w Vue 3 dla kontroli dostępu opartego na rolach wyjaśnieni przy użyciu praktycznych wzorców: reguły w `meta` trasy, bezpieczne przekierowania, przyjazne fallbacki 401/403 oraz sposoby unikania wycieków danych.

Co faktycznie rozwiązują strażnicy tras (a czego nie rozwiązują)
Strażnicy tras robią jedną rzecz dobrze: kontrolują nawigację. Decydują, czy ktoś może wejść na daną trasę i gdzie go wysłać, jeśli nie może. To poprawia UX, ale nie zastępuje bezpieczeństwa.
Ukrycie elementu menu to tylko wskazówka, nie autoryzacja. Ludzie wciąż mogą wpisać URL, odświeżyć stronę z głębokim linkiem lub otworzyć zakładkę. Jeśli jedyną ochroną jest „przycisk nie jest widoczny”, nie masz ochrony.
Strażnicy sprawdzają się, gdy chcesz, aby aplikacja zachowywała się spójnie, blokując strony, które nie powinny być widoczne — np. obszary admina, narzędzia wewnętrzne czy portale klientów oparte na rolach.
Strażnicy pomagają Ci:
- Zablokować strony przed renderowaniem
- Przekierować do logowania lub bezpiecznego domyślnego widoku
- Pokazać czytelną stronę 401/403 zamiast uszkodzonego widoku
- Unikać przypadkowych pętli nawigacyjnych
Czego strażnicy nie potrafią zrobić sami: chronić danych. Jeśli API zwraca wrażliwe dane do przeglądarki, użytkownik nadal może wywołać to endpoint bezpośrednio (albo przejrzeć odpowiedzi w narzędziach deweloperskich), nawet jeśli strona jest zablokowana. Prawdziwa autoryzacja musi działać również po stronie serwera.
Dobrym celem jest zabezpieczenie obu stron: blokowanie stron i blokowanie danych. Jeśli agent wsparcia otworzy trasę dostępną tylko dla administratora, strażnik powinien zatrzymać nawigację i wyświetlić „Dostęp zabroniony”. Równolegle backend powinien odmawiać wywołań API dostępnych tylko dla adminów, żeby wrażliwe dane nigdy nie zostały zwrócone.
Wybierz prosty model ról i uprawnień
Kontrola dostępu robi się skomplikowana, gdy zaczynasz od długiej listy ról. Zacznij od niewielkiego zestawu, który ludzie naprawdę rozumieją, i dodawaj bardziej szczegółowe uprawnienia dopiero wtedy, gdy poczujesz rzeczywisty ból.
Praktyczny podział to:
- Role opisują, kim ktoś jest w aplikacji.
- Uprawnienia opisują, co może robić.
Dla większości narzędzi wewnętrznych trzy role wystarczają na początek:
- admin: zarządza użytkownikami i ustawieniami, widzi wszystkie dane
- support: obsługuje rekordy klientów i odpowiedzi, ale nie ustawia systemu
- viewer: dostęp tylko do odczytu do zatwierdzonych ekranów
Zdecyduj wcześnie, skąd pochodzą role. Claimy w tokenie (JWT) są szybkie dla strażników, ale mogą być nieaktualne, dopóki token nie zostanie odświeżony. Pobranie profilu użytkownika przy starcie aplikacji daje zawsze aktualne dane, ale strażnicy muszą poczekać, aż to żądanie się zakończy.
Wyraźnie rozdziel typy tras: publiczne (dostępne dla wszystkich), wymagające uwierzytelnienia (wymagają sesji) i zastrzeżone (wymagają roli lub uprawnienia).
Definiuj reguły dostępu za pomocą meta trasy
Najczystszy sposób wyrażenia dostępu to zadeklarowanie go bezpośrednio przy trasie. Vue Router pozwala dołączyć obiekt meta do rekordu trasy, aby strażnicy mogli go później odczytać. Dzięki temu reguły są blisko stron, które chronią.
Wybierz prosty kształt meta i trzymaj się go w całej aplikacji.
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 },
},
]
Dla tras zagnieżdżonych zdecyduj, jak reguły się łączą. W większości aplikacji dzieci powinny dziedziczyć wymagania rodzica. W strażniku sprawdzaj każdy dopasowany rekord trasy (nie tylko to.meta), żeby nie pominąć wymagań rodzica.
Jedna rzecz, która oszczędzi czasu później: rozróżnij „może zobaczyć” i „może edytować”. Trasa może być widoczna dla supportu i adminów, ale edycje powinny być zablokowane dla supportu. Flaga readOnly: true w meta może sterować zachowaniem UI (wyciszać akcje, ukrywać przyciski destrukcyjne) bez udawania, że to jest zabezpieczenie.
Przygotuj stan auth, żeby strażnicy zachowywali się przewidywalnie
Większość błędów ze strażnikami wynika z jednego problemu: strażnik uruchamia się zanim aplikacja pozna użytkownika.
Traktuj auth jak mały automat stanów i zrób z niego pojedyncze źródło prawdy. Chcesz mieć trzy jasne stany:
- unknown: aplikacja właśnie wystartowała, sesja nie została jeszcze sprawdzona
- logged out: sprawdzenie sesji zakończone, brak ważnego użytkownika
- logged in: użytkownik załadowany, role/uprawnienia dostępne
Zasada: nigdy nie czytaj ról, gdy auth jest unknown. To właśnie powoduje błyski chronionych ekranów lub niespodziewane przekierowania do logowania.
Zdecyduj, jak działa odświeżanie sesji
Wybierz jedną strategię odświeżania i trzymaj się jej (np. odczytaj token, wywołaj endpoint „who am I”, ustaw użytkownika).
Stabilny wzorzec wygląda tak:
- Przy starcie aplikacji ustaw auth na unknown i rozpocznij pojedyncze żądanie odświeżające
- Rozwiązuj strażników dopiero po zakończeniu (lub wygaśnięciu) tego żądania
- Cache'uj użytkownika w pamięci, nie w
metatrasy - W razie błędu ustaw auth na logged out
- Udostępnij obietnicę
ready(lub podobny mechanizm), na którą strażnicy mogą czekać
Po wdrożeniu tego wzorca logika strażnika pozostaje prosta: poczekaj, aż auth będzie gotowy, a potem podejmij decyzję o dostępie.
Krok po kroku: implementacja autoryzacji na poziomie trasy
Czyste podejście to trzymać większość reguł w jednym globalnym strażniku, a używać per-trasa beforeEnter tylko wtedy, gdy trasa naprawdę potrzebuje specjalnej logiki.
1) Dodaj globalny guard beforeEach
// 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
})
To pokrywa większość przypadków bez rozrzucania sprawdzeń po komponentach.
Kiedy lepszy jest beforeEnter
Używaj beforeEnter, gdy reguła jest naprawdę specyficzna dla trasy, np. „tylko właściciel zgłoszenia może otworzyć tę stronę” i zależy od to.params.id. Trzymaj go krótko i korzystaj z tego samego store auth, żeby zachowanie było spójne.
Bezpieczne przekierowania bez otwierania luk
Przekierowania mogą cicho podważyć Twoją kontrolę dostępu, jeśli traktujesz je jako zaufane.
Częsty wzorzec: gdy użytkownik jest wylogowany, wysyłasz go do logowania i dołączasz parametr query returnTo. Po zalogowaniu odczytujesz go i nawigujesz tam. Ryzyko to open redirects (wysyłanie użytkowników poza aplikację) i pętle.
Trzymaj zachowanie proste:
- Wylogowani użytkownicy idą do
LoginzreturnToustawionym na bieżącą ścieżkę. - Zalogowani, lecz nieuprawnieni użytkownicy idą na dedykowaną stronę
Forbidden(nie naLogin). - Pozwalaj tylko na wewnętrzne wartości
returnTo, które rozpoznajesz. - Dodaj jedną kontrolę pętli, żeby nigdy nie przekierować w kółko.
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' }
}
})
Unikaj wycieku zastrzeżonych danych podczas nawigacji
Najprostszy wyciek to ładowanie danych zanim wiesz, czy użytkownik ma do nich dostęp.
W Vue często zdarza się to, gdy strona pobiera dane w setup(), a strażnik wykona się chwilę później. Nawet jeśli użytkownik zostanie przekierowany, odpowiedź może trafić do współdzielonego store lub pokazać się przez moment na ekranie.
Bezpieczniejsza zasada: autoryzuj najpierw, potem ładuj.
// 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' }
}
})
Uważaj też na opóźnione żądania, gdy nawigacja zmienia się szybko. Anuluj żądania (np. przez AbortController) lub ignoruj późne odpowiedzi, sprawdzając identyfikator żądania.
Cache to kolejna pułapka. Jeśli przechowujesz „ostatnio załadowany rekord klienta” globalnie, odpowiedź dostępna tylko dla admina może później być pokazana zwykłemu użytkownikowi, który odwiedzi ten sam szablon ekranu. Klucze cache'uj według id użytkownika i roli oraz czyść wrażliwe moduły przy wylogowaniu (lub gdy role się zmieniają).
Kilka praktyk zapobiegających większości wycieków:
- Nie pobieraj wrażliwych danych, dopóki autoryzacja nie zostanie potwierdzona.
- Keyuj cache według użytkownika i roli lub trzymaj dane lokalnie na stronie.
- Anuluj lub ignoruj żądania w locie przy zmianie trasy.
Przyjazne fallbacki: 401, 403 i nie znaleziono
Ścieżki „nie” są tak samo ważne jak ścieżki „tak”. Dobre strony awaryjne utrzymują orientację użytkownika i zmniejszają liczbę zgłoszeń do wsparcia.
401: Wymagane logowanie (nie uwierzytelniony)
Używaj 401, gdy użytkownik nie jest zalogowany. Komunikat trzymaj prosty: musi się zalogować, aby kontynuować. Jeśli wspierasz powrót na oryginalną stronę po logowaniu, waliduj ścieżkę powrotu, aby nie mogła wskazywać poza aplikację.
403: Brak dostępu (uwierzytelniony, ale nieuprawniony)
Używaj 403, gdy użytkownik jest zalogowany, ale nie ma uprawnień. Zachowaj komunikat neutralny i nie sugeruj wrażliwych szczegółów.
Solidna strona 403 zwykle ma czytelny tytuł („Dostęp zabroniony”), jedno zdanie wyjaśnienia i bezpieczny następny krok (powrót do pulpitu, kontakt z administratorem, zmiana konta jeśli dostępne).
404: Nie znaleziono
Obsługuj 404 oddzielnie od 401/403. W przeciwnym razie ludzie będą myśleć, że brakuje im uprawnień, gdy strona po prostu nie istnieje.
Typowe błędy, które łamią kontrolę dostępu
Większość błędów kontroli dostępu to proste pomyłki logiczne, które objawiają się pętlami przekierowań, błyskami niewłaściwych ekranów lub zablokowaniem użytkownika.
Zwykłe przyczyny:
- Traktowanie ukrytego UI jako „bezpieczeństwa”. Zawsze egzekwuj role w routerze i w API.
- Odczytywanie ról ze starego stanu po wylogowaniu/logowaniu.
- Przekierowywanie nieuprawnionych użytkowników na inną chronioną trasę (natychmiastowa pętla).
- Ignorowanie momentu „auth się jeszcze ładuje” przy odświeżeniu.
- Mylące 401 i 403, co dezorientuje użytkowników.
Realistyczny przykład: agent wsparcia wylogowuje się, a na tym samym współdzielonym komputerze loguje się administrator. Jeśli strażnik odczyta zcache'owaną rolę zanim nowa sesja zostanie potwierdzona, możesz błędnie zablokować admina lub — co gorsza — chwilowo pozwolić na dostęp, którego nie powinien mieć.
Szybka lista kontrolna przed wydaniem
Zrób krótkie przejście skupione na chwilach, w których kontrola dostępu zwykle zawodzi: wolne sieci, wygasłe sesje i zapisane adresy URL.
- Każda chroniona trasa ma jawne wymagania w
meta. - Strażnicy obsługują stan ładowania auth bez błysków chronionego UI.
- Nieuprawnieni użytkownicy trafiają na czytelny ekran 403 (nie mylący powrót do strony głównej).
- Każde przekierowanie „powrót do” jest walidowane i nie tworzy pętli.
- Wrażliwe wywołania API uruchamiane są dopiero po potwierdzonej autoryzacji.
Następnie przetestuj jeden scenariusz end-to-end: otwórz chroniony URL w nowej karcie będąc wylogowanym, zaloguj się jako zwykły użytkownik i potwierdź, że trafiasz na stronę, do której masz dostęp, albo na czytelny 403 z instrukcją dalszego kroku.
Przykład: dostęp supportu vs admina w małej aplikacji webowej
Wyobraź sobie aplikację helpdesk z dwiema rolami: support i admin. Support może czytać i odpowiadać na zgłoszenia. Admin może robić to samo, plus zarządzać billingiem i ustawieniami firmy.
/tickets/:idjest dostępne dlasupportiadmin/settings/billingjest dostępne tylko dlaadmin
Typowy scenariusz: agent supportu otwiera stary deep link do /settings/billing z zakładki. Strażnik powinien sprawdzić meta trasy zanim komponent się załaduje i zablokować nawigację. Ponieważ użytkownik jest zalogowany, ale nie ma roli, powinien trafić na bezpieczny fallback (403).
Dwa komunikaty mają znaczenie:
- Wymagane logowanie (401): „Zaloguj się, aby kontynuować.”
- Dostęp zabroniony (403): „Nie masz dostępu do ustawień rozliczeń.”
Co nie może się wydarzyć: komponent billing mountuje się lub dane billingowe są pobrane, nawet przez chwilę.
Zmiany ról w trakcie sesji to kolejny przypadek brzegowy. Jeśli ktoś zostanie awansowany lub zdegradowany, nie polegaj na samym menu. Ponownie sprawdzaj role przy nawigacji i zdecyduj, jak obsłużysz aktywne strony: odśwież stan auth przy zmianie profilu lub wykrywaj zmiany ról i przekierowuj z stron, które nie są już dozwolone.
Następne kroki: utrzymuj reguły dostępu w porządku
Gdy strażnicy działają, większym ryzykiem jest dryf: nowa trasa wypuszczona bez meta, rola zmieniona nazwa i reguły stają się niespójne.
Zamień swoje reguły w mały plan testów, który uruchomisz za każdym razem, gdy dodajesz trasę:
- Jako Gość: otwórz chronione trasy i potwierdź, że trafiasz na login bez widocznej częściowej zawartości.
- Jako Użytkownik: otwórz stronę, do której nie powinieneś mieć dostępu i potwierdź czytelne 403.
- Jako Admin: przetestuj deep linki kopiowane z paska adresu.
- Dla każdej roli: odśwież na chronionej trasie i potwierdź stabilny rezultat.
Jeśli chcesz dodatkowe zabezpieczenie, dodaj widok developerski lub output w konsoli, który wypisuje trasy i ich wymagania meta, dzięki czemu brakujące reguły będą od razu widoczne.
Jeżeli budujesz narzędzia wewnętrzne lub portale z AppMaster (appmaster.io), możesz zastosować to samo podejście: trzymaj strażników skupionych na nawigacji w UI Vue3 i egzekwuj uprawnienia tam, gdzie leży logika backendu i dane.
Wybierz jedną rzecz do poprawy i wdroż ją end-to-end: zaostrz bramkowanie pobierania danych, ulepsz stronę 403 albo zablokuj obsługę przekierowań. Małe poprawki to te, które zatrzymują większość realnych błędów dostępu.
FAQ
Strażnicy routingu kontrolują nawigację, a nie dostęp do danych. Pomagają zablokować stronę, przekierować i wyświetlić czytelny stan 401/403, ale nie powstrzymają kogoś przed bezpośrednim wywołaniem API. Zawsze egzekwuj te same uprawnienia po stronie serwera, aby wrażliwe dane nigdy nie były zwracane.
Ukrywanie elementu interfejsu zmienia tylko to, co ktoś widzi, a nie to, co może zażądać. Użytkownicy wciąż mogą wpisać adres URL, otworzyć zakładkę lub użyć deep linku. Musisz mieć sprawdzenia w routerze, by zablokować stronę, oraz autoryzację po stronie serwera, by zablokować dane.
Zacznij od małego, zrozumiałego zestawu ról, a dopiero potem dodawaj uprawnienia, gdy poczujesz ból. Typowy zestaw to admin, support i viewer, a potem dodajesz uprawnienia jak tickets:read czy audit:read dla konkretnych akcji. Rozdziel „kim jesteś” (rola) od „co możesz zrobić” (uprawnienie).
Umieszczaj reguły dostępu w meta rekordów routingu, np. requiresAuth, roles i permissions. Dzięki temu zasady są blisko stron, które chronią, i globalny guard działa przewidywalnie. Dla zagnieżdżonych tras sprawdzaj wszystkie dopasowane rekordy, żeby nie pominąć wymagań rodzica.
Czytaj to.matched i łącz wymagania ze wszystkich dopasowanych rekordów routingu. W ten sposób trasa potomna nie ominie requiresAuth lub roles ustawionego przez rodzica. Ustal wcześniej jasną regułę łączenia (zwykle: wymagania rodzica obowiązują dla dzieci).
Częstym powodem „flashy” lub pętli przekierowań jest to, że guard działa zanim aplikacja pozna użytkownika. Traktuj auth jako trzy stany — unknown, logged out, logged in — i nigdy nie oceniaj ról, gdy auth jest unknown. Spraw, by guard czekał na inicjalizację (np. jedno żądanie „who am I”) przed podjęciem decyzji.
Domyślnie używaj globalnego beforeEach dla reguł typu „wymaga logowania” lub „wymaga roli/uprawnienia”. beforeEnter stosuj tylko wtedy, gdy reguła jest naprawdę specyficzna dla trasy i zależy od parametrów, np. „tylko właściciel zgłoszenia może otworzyć tę stronę”. Oba mechanizmy korzystają z tego samego źródła prawdy o auth.
Traktuj returnTo jako niezaufane wejście. Pozwalaj tylko na wewnętrzne ścieżki, które rozpoznajesz (np. zaczynające się od / i pasujące do znanych prefiksów) i dodaj kontrolę pętli, żeby nie przekierować z powrotem na tę samą zablokowaną trasę. Użytkownicy niezalogowani idą do Login; zalogowani, lecz nieuprawnieni — na dedykowaną stronę 403.
Autoryzuj zanim pobierzesz. Jeśli strona robi fetch w setup() i chwilę później następuje przekierowanie, odpowiedź może trafić do globalnego store lub chwilowo pojawić się na ekranie. Odmierz wrażliwe żądania dopiero po potwierdzonej autoryzacji i anuluj albo ignoruj przeterminowane odpowiedzi przy zmianie trasy.
Używaj 401, gdy użytkownik nie jest zalogowany, 403 gdy jest zalogowany, ale nie ma uprawnień. Traktuj 404 oddzielnie, by użytkownicy nie mylili braku strony z brakiem dostępu. Jasne i konsekwentne fallbacky zmniejszają liczbę zgłoszeń do działu wsparcia.


