Hubert
14 min
4 czerwca, 2025

Autoryzacja i ochrona tras w ExpoRouter – jak budować logikę dostępów?

W każdej aplikacji mobilnej, która zawiera logowanie, panel użytkownika czy ekran ustawień, pojawia się potrzeba ograniczenia dostępu do wybranych ekranów. Użytkownik niezalogowany nie powinien widzieć danych konta, a zalogowany — ponownie trafiać na ekran logowania. Właśnie za to odpowiada tzw. ochrona tras (route protection). Jeśli korzystasz z ExpoRouter w projekcie React Native, masz do dyspozycji kilka sposobów zabezpieczania tras: od prostych warunków w komponentach po bardziej eleganczne rozwiązania z użyciem middleware. W tym artykule pokażę Ci, jak zaimplementować autoryzację i ochronę tras w ExpoRouter, kiedy warto korzystać z poszczególnych podejść oraz jak uniknąć typowych błędów, które pojawiają się w prawdziwych projektach.

Czytaj więcej
Autoryzacja i ochrona tras w ExpoRouter – jak budować logikę dostępów?

Co to znaczy „ochrona trasy”?

Ochrona trasy (ang. route protection) to kontrola dostępu do konkretnego ekranu lub grupy ekranów w aplikacji — w oparciu o kontekst użytkownika, jego stan logowania, rolę, czy inne warunki biznesowe. W architekturze frontendu oznacza to świadome zarządzanie tym, kto ma prawo zobaczyć dany widok i kiedy.

Przykładowe scenariusze ochrony tras:

– użytkownik niezalogowany próbuje wejść na /dashboard → przekierowanie na /login.
– użytkownik zalogowany wchodzi na /login → przekierowanie na /dashboard, bo nie ma sensu go tam trzymać.
– użytkownik bez uprawnień admina próbuje otworzyć /admin/settings → przekierowanie lub wyświetlenie komunikatu o braku dostępu.
– aplikacja czeka na weryfikację tokenu → trasa jest tymczasowo „zablokowana”, np. przez ekran loadingowy.

Jaki jest cel ochrony trasy?

Ochrona tras to nie tylko kwestia UX — to kluczowy element bezpieczeństwa aplikacji. Choć aplikacja mobilna działa po stronie frontendu (a wrażliwe dane powinny być zawsze chronione przez backend), to właściwe zarządzanie dostępem do tras ma istotne znaczenie. Po pierwsze, zapobiega przypadkowemu wyciekowi danych, na przykład przez chwilowe wyświetlenie poufnych informacji zanim zostanie zweryfikowany token użytkownika. Po drugie, ogranicza ryzyko błędów w logice dostępu, takich jak umożliwienie nieautoryzowanemu użytkownikowi skorzystania z funkcji przeznaczonej dla innej roli. Po trzecie, wpływa pozytywnie na płynność działania aplikacji — użytkownik nie musi samodzielnie wracać do ekranu logowania, jeśli jego sesja wygasła, ponieważ aplikacja automatycznie reaguje na ten stan.

Potrzebujesz wsparcia w projektach mobilnych?
Potrzebujesz wsparcia w projektach mobilnych?
Potrzebujesz wsparcia w projektach mobilnych?
Napisz do nas!

Tworzenie logiki autoryzacji – źródło prawdy

Każdy system autoryzacji opiera się na jednym kluczowym fundamencie: wiarygodnym źródle informacji o stanie użytkownika. Bez względu na to, czy aplikacja korzysta z JWT, sesji, tokenów OAuth czy własnego rozwiązania — frontend musi wiedzieć, czy użytkownik jest zalogowany, jakie ma uprawnienia oraz czy ten stan został już jednoznacznie zweryfikowany. Właśnie tę rolę pełni tzw. źródło prawdy (single source of truth) dla autoryzacji.

W praktyce, w aplikacjach mobilnych budowanych z ExpoRouterem, takim źródłem jest najczęściej globalny stan – np. React Context, Zustand, Redux, lub w prostszych przypadkach: zmienna lokalna w komponencie nadrzędnym layoutu. To tam przechowujemy informacje o użytkowniku, tokenie dostępu, jego roli, a także statusie ładowania tych danych.

Ważne jest, aby od samego początku rozdzielić trzy stany logiczne:

1. Użytkownik jest zalogowany i autoryzowany – możemy pokazywać zawartość chronionych tras.

2. Użytkownik jest niezalogowany – należy przekierować go np. do ekranu logowania.

3. Stan autoryzacji jest jeszcze nieznany – np. aplikacja dopiero sprawdza zapisany token w SecureStore lub AsyncStorage.

To trzecie rozróżnienie jest szczególnie istotne. Pomijanie stanu „ładowania” prowadzi do błędów typu „flash” – ekran przez ułamek sekundy pokazuje zawartość, zanim zdąży zareagować na rzeczywisty brak autoryzacji.

Dobrą praktyką jest utworzenie prostego obiektu lub hooka useAuth, który udostępnia takie właściwości jak isAuthenticated, user, isLoading oraz funkcje typu login() i logout(). Dzięki temu komponenty mogą w przewidywalny sposób podejmować decyzje o tym, co renderować — albo czy w ogóle powinny być dostępne.

W kolejnej części pokażę, jak wykorzystać tę logikę w praktyce – najpierw globalnie, za pomocą middleware.ts, by przechwytywać nieautoryzowane próby wejścia na daną trasę zanim zostanie załadowany ekran.

Ochrona tras za pomocą middleware

Jednym z najmocniejszych narzędzi w ExpoRouterze, jeśli chodzi o ochronę tras, jest middleware. To specjalna funkcja, która uruchamia się zanim użytkownik zostanie przeniesiony na daną stronę – umożliwia weryfikację stanu aplikacji, a następnie przekierowanie, kontynuację lub przerwanie przejścia między trasami. W kontekście autoryzacji pozwala to np. zablokować dostęp do panelu użytkownika, jeśli nie jest zalogowany, lub odwrotnie – uniemożliwić zalogowanemu użytkownikowi wejście na ekran logowania.

Jak działa middleware w ExpoRouter?

W projekcie korzystającym z ExpoRoutera (od v2), możesz utworzyć plik middleware.ts w katalogu głównym /app. Eksportujesz z niego funkcję middleware(request), która otrzymuje kontekst żądania — m.in. ścieżkę, do której użytkownik chce przejść.

W środku tej funkcji możesz zaimplementować dowolną logikę decyzyjną — np. sprawdzić, czy użytkownik jest zalogowany, i jeśli nie, przekierować go na /login. Middleware działa na poziomie routingu, zanim załadowany zostanie docelowy komponent, co czyni je wydajnym i „czystym” miejscem na sprawdzanie autoryzacji.

Przykład: blokowanie dostępu do /dashboard dla niezalogowanych

// app/middleware.ts
import { NextRequest, NextResponse } from 'expo-router/server';

export function middleware(request: NextRequest) {
  const isAuthenticated = checkIfUserIsAuthenticated(); // <- Twoja funkcja z logiką autoryzacji

  const pathname = request.nextUrl.pathname;

  if (!isAuthenticated && pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  if (isAuthenticated && pathname === '/login') {
    return NextResponse.redirect(new URL('/dashboard', request.url));
  }

  return NextResponse.next();
}

W prawdziwej aplikacji funkcja checkIfUserIsAuthenticated() będzie bazować np. na tokenie zapisanym w pamięci (np. SecureStore, async context) — ale ważne jest, żeby ten stan był dostępny natychmiast, ponieważ middleware działa synchronicznie. To oznacza, że nie możemy czekać na asynchroniczne sprawdzenie tokenu – potrzebujemy wstępnie załadowanego stanu autoryzacji, np. poprzez inicjalizację globalnego store’u w app/_layout.tsx.

Zalety middleware

Największą zaletą middleware jest to, że działa przed renderowaniem komponentu, co eliminuje problemy z „migającym” widokiem (czyli sytuacją, w której chroniona treść na chwilę się pokazuje). Dodatkowo, centralizuje logikę autoryzacji — nie musisz rozpraszać warunków po wszystkich komponentach.

O czym trzeba pamiętać?

Middleware nie może korzystać z hooków (useAuth, useStore, itp.) ani żadnych async funkcji (np. SecureStore.getItemAsync()), więc musisz zadbać, by decyzje autoryzacyjne opierały się na stanie dostępnym natychmiast — np. z pamięci synchronizowanej przy starcie aplikacji. Jeśli nie jesteś w stanie tego zagwarantować, middleware nie będzie właściwym miejscem do ochrony tras i lepiej użyć warunków w komponentach.

Alternatywa: ochrona tras w layoutach lub komponentach

Choć middleware w ExpoRouterze jest bardzo przydatne, nie zawsze jest możliwe jego wykorzystanie — głównie dlatego, że działa synchronicznie i nie obsługuje async operacji. W praktyce wiele aplikacji mobilnych potrzebuje najpierw odczytać token z SecureStore, sprawdzić jego ważność z backendem lub poczekać na inicjalizację globalnego stanu. W takich przypadkach znacznie lepiej sprawdza się ochrona tras na poziomie komponentów lub layoutów.

Jak to działa?

Zamiast zatrzymywać użytkownika przed wejściem na stronę, pozwalamy mu tam wejść, ale warunkowo kontrolujemy, co się wyświetli – zależnie od stanu autoryzacji. Dzięki temu możemy bezpiecznie korzystać z hooków (useAuth, useStore, useEffect), a także asynchronicznie pobierać potrzebne dane lub odczytywać token z pamięci urządzenia.

Typowa implementacja wygląda tak: komponent layoutu lub strona sprawdza, czy użytkownik jest zalogowany. Jeśli nie — wykonuje przekierowanie lub wyświetla placeholder, loader albo ekran błędu.

Przykład: ochrona layoutu app/(protected)/_layout.tsx

import { useRouter } from 'expo-router';
import { useAuth } from '@/lib/useAuth'; // custom hook z logiką autoryzacji
import { useEffect } from 'react';

export default function ProtectedLayout({ children }: { children: React.ReactNode }) {
  const { isAuthenticated, isLoading } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!isLoading && !isAuthenticated) {
      router.replace('/login');
    }
  }, [isLoading, isAuthenticated]);

  if (isLoading) {
    return null; // lub loader
  }

  return <>{children}</>;
}

To podejście działa niezawodnie nawet wtedy, gdy stan autoryzacji nie jest jeszcze znany w momencie startu aplikacji — bo layout może poczekać na załadowanie, a dopiero potem podjąć decyzję.

Kiedy to lepsze niż middleware?

To podejście sprawdzi się szczególnie dobrze, gdy:

  • autoryzacja zależy od danych asynchronicznych (np. sprawdzenie tokenu z backendem),
  • chcesz w bardziej granularny sposób kontrolować dostępność fragmentów UI (np. różne widoki dla różnych ról),
  • potrzebujesz czasowego renderowania placeholderów (np. ekran ładowania, ekran błędu, komunikat o wygaśniętej sesji).

Potencjalne pułapki

Najczęstszy błąd to niewystarczające zabezpieczenie przed migającym UI, gdy warunki autoryzacji nie są jeszcze spełnione. W takim przypadku ekran z treścią może na chwilę się pojawić, zanim nastąpi redirect. Dlatego kluczowe jest poprawne zarządzanie flagą isLoading – aplikacja powinna poczekać z renderowaniem zawartości do momentu, gdy autoryzacja została jednoznacznie określona.

Co może pójść nie tak? Najczęstsze błędy

Implementacja ochrony tras wydaje się prosta w teorii, ale w praktyce wiąże się z wieloma subtelnymi problemami, zwłaszcza w środowisku mobilnym, gdzie stan aplikacji może być niepełny, opóźniony lub chwilowo niedostępny. Oto najczęstsze błędy, które mogą pojawić się przy zabezpieczaniu tras w ExpoRouterze — zarówno w middleware, jak i na poziomie komponentów:

Migające widoki (auth flash)

Jednym z najczęstszych błędów jest tzw. „flash” nieautoryzowanego widoku. Dzieje się tak, gdy aplikacja próbuje od razu wyrenderować zawartość chronionej trasy, zanim jeszcze zostanie potwierdzony stan użytkownika. W efekcie użytkownik może przez chwilę zobaczyć dane, do których nie powinien mieć dostępu. To nie tylko zły UX, ale też potencjalny problem bezpieczeństwa.

Aby tego uniknąć, należy wprowadzić mechanizm wstrzymujący renderowanie do czasu, aż autoryzacja będzie jednoznacznie potwierdzona. W praktyce sprowadza się to do użycia flagi typu isLoading lub authReady w globalnym stanie.

Pętle przekierowań

Błędne warunki redirectu mogą łatwo doprowadzić do nieskończonego cyklu nawigacji między trasami. Klasyczny przypadek: niezalogowany użytkownik trafia na /dashboard, więc aplikacja przekierowuje go na /login — ale komponent loginu też odczytuje brak autoryzacji i ponownie próbuje przenieść użytkownika z powrotem na /dashboard. I tak w kółko. Rozwiązaniem jest precyzyjne i rozłączne określenie warunków — np. tylko niezalogowany użytkownik na stronie chronionej → redirect do loginu; natomiast zalogowany użytkownik na loginie → redirect do dashboardu. Każdy przypadek powinien być jednoznacznie obsłużony.

Próba użycia async w middleware

Middleware działa synchronicznie. To ważne, bo oznacza, że nie można w nim używać operacji asynchronicznych, takich jak SecureStore.getItemAsync() czy żądań HTTP. Deweloperzy próbujący na tym etapie odczytać token lub pobrać dane użytkownika z backendu napotkają błędy lub nieprzewidywalne efekty.

Dlatego przed uruchomieniem middleware warto zadbać o preload stanu autoryzacji. Można to zrobić np. w _layout.tsx, inicjalizując store globalny podczas ładowania aplikacji.

Opóźnione ładowanie stanu

Czasami logika auth została zaimplementowana poprawnie, ale ładowanie informacji o użytkowniku trwa zbyt długo. Efekt? Przez kilka sekund użytkownik może przeglądać elementy UI, których nie powinien widzieć. A jeśli w tym czasie kliknie przycisk, wykona akcję, która nie była mu przeznaczona.

To jeden z bardziej podstępnych problemów, bo często nie wychodzi na jaw w testach lokalnych. Dlatego warto stosować podejście defensywne: jeśli auth nie jest gotowy — nie renderujemy nic. Lepiej pokazać przez chwilę pusty ekran niż ujawnić poufne dane lub dostęp do nieautoryzowanej funkcji.

Zbyt uproszczona walidacja użytkownika

Ostatnia, często bagatelizowana kwestia to poleganie wyłącznie na istnieniu obiektu user. Wielu deweloperów uznaje user !== null za wystarczający dowód autoryzacji, ignorując fakt, że sam obiekt może być niekompletny. Przykład: user istnieje, ale nie ma przypisanej roli lub flagi, która pozwala określić uprawnienia.

Warto w takich przypadkach wprowadzić dodatkową warstwę walidacji — np. hasAccess(user, route) lub user.role === 'admin'. To pozwala uniknąć sytuacji, w której użytkownik z ograniczonym dostępem może przypadkiem wejść w obszar administracyjny aplikacji.

Testowanie i debugowanie ochrony tras

Dobra architektura autoryzacji to jedno. Jej niezawodność w warunkach produkcyjnych — to zupełnie inna sprawa. W praktyce nawet elegancko napisana logika ochrony tras może zawieść, jeśli nie zostanie porządnie przetestowana. A ponieważ mamy do czynienia z dynamicznie zmieniającym się stanem — często inicjalizowanym asynchronicznie — testy muszą obejmować coś więcej niż tylko „czy działa na moim simulatorze”. Zacznij od najprostszych przypadków. Użytkownik niezalogowany wchodzi na trasę chronioną — czy faktycznie jest natychmiast przekierowywany? A co jeśli otworzy aplikację bezpośrednio na ekranie z linku, np. /dashboard, zanim auth się załaduje? W takich sytuacjach objawiają się błędy, których nie widać w codziennej pracy — użytkownik widzi na sekundę coś, czego nie powinien, albo redirect zachodzi z wyczuwalnym opóźnieniem.

W przypadku middleware sytuacja jest nieco inna. Tam wszystko dzieje się synchronicznie, więc testy sprowadzają się do sprawdzania, czy logika warunków działa poprawnie. Nie możesz jednak podejrzeć wartości isAuthenticated czy user.role tak łatwo jak w komponencie — musisz mieć sposób, żeby dostarczyć gotowy stan w momencie uruchamiania middleware. Dlatego tak ważne jest wcześniejsze preloadowanie danych, najlepiej już w momencie startu aplikacji. W przeciwnym razie będziesz testować middleware na „pustym” stanie, co da fałszywe wyniki.

Testując ochronę tras w komponentach, masz większą kontrolę. Możesz logować wartości, reagować na zmiany auth, debugować w kontekście hooków i podejrzeć, co dzieje się na poziomie UI. Warto jednak pamiętać, że redirecty wykonują się zazwyczaj w useEffect, czyli po pierwszym renderze. Jeśli nie wstrzymasz renderowania zawartości do czasu ustalenia stanu, ryzykujesz tzw. auth flash — czyli przez chwilę pojawia się treść, której użytkownik nie powinien widzieć. To z pozoru drobiazg, ale w aplikacjach z wrażliwymi danymi lub kontrolą ról jest to poważny problem. Nie ograniczaj testów tylko do typowych flow. W symulacjach warto też zasymulować skrajne przypadki: co się stanie, jeśli użytkownik jest zalogowany, ale ma token wygasły? A jeśli nie ma dostępu do internetu i nie można potwierdzić sesji? Czy aplikacja wtedy zatrzyma się na ekranie ładowania, czy pozwoli „przeskoczyć” do tras, które powinny być zablokowane?

Testuj na fizycznym urządzeniu, a nie tylko emulatorze. Sprawdź zachowanie po resecie aplikacji, po ponownym uruchomieniu i po dłuższej nieaktywności. To właśnie tam najczęściej wychodzą problemy z cache’em, race conditionami i nieprzewidywalnym zachowaniem redirectów.

Dobre testowanie logiki auth to nie tylko upewnienie się, że „redirect działa”. To sprawdzenie, czy aplikacja nigdy nie pokazuje treści użytkownikowi, który nie powinien jej widzieć — nawet przez chwilę. Jeśli spełnisz ten warunek w każdych warunkach, możesz mieć pewność, że ochrona tras została zaimplementowana solidnie.

Podsumowanie

Ochrona tras w aplikacjach zbudowanych na ExpoRouterze to coś więcej niż tylko warunek if (!isAuthenticated). To przemyślany system, który łączy architekturę stanu, flow użytkownika i bezpieczeństwo. W tym artykule pokazaliśmy dwa główne podejścia — ochronę tras przez middleware oraz przez komponenty lub layouty — każde z nich ma swoje zalety i ograniczenia.

Middleware działa szybko i czysto, zanim cokolwiek zostanie wyrenderowane, ale wymaga synchronizacji stanu już na starcie. Komponenty z kolei dają pełną elastyczność i dostęp do hooków, ale muszą umiejętnie radzić sobie z asynchronicznością, by nie dopuścić do „migających” widoków. Wybór między nimi zależy głównie od tego, jak zbudowana jest logika autoryzacji w Twojej aplikacji — i czy jesteś w stanie zapewnić natychmiastowy dostęp do stanu auth.

Jeśli jest jedna rzecz, którą warto zapamiętać z tego materiału, to fakt, że ochrona tras nie kończy się na przekierowaniu. To systemowa odpowiedzialność aplikacji za to, żeby użytkownik nigdy nie widział tego, czego nie powinien, niezależnie od tego, w jaki sposób się tam dostał.

Zadbaj o dobre testy, uwzględnij stany przejściowe i myśl o edge-case’ach. Dzięki temu Twoja aplikacja będzie nie tylko bezpieczna, ale też bardziej przewidywalna i odporna na błędy.

FAQ – Najczęściej zadawane pytania

1. Czy powinienem zawsze używać middleware do ochrony tras?
Nie zawsze. Middleware sprawdza się, gdy masz synchronizowany stan auth już w momencie startu aplikacji. Jeśli autoryzacja opiera się na danych asynchronicznych (np. token z SecureStore), lepszym rozwiązaniem będzie ochrona tras bezpośrednio w komponentach lub layoutach.

2. Czy można używać async/await w middleware?
Nie. Middleware w ExpoRouter działa synchronicznie i nie obsługuje operacji asynchronicznych. Próba użycia await spowoduje błąd lub nieoczekiwane zachowanie.

3. Jak uniknąć „flashowania” treści dla niezalogowanego użytkownika?
Zadbaj o to, by nie renderować zawartości chronionej trasy, dopóki status autoryzacji nie został jednoznacznie określony. W praktyce oznacza to: if (isLoading) return null; i dopiero później warunek autoryzacyjny.

4. Czy muszę osobno chronić każdą trasę?
Nie. Warto pogrupować trasy wymagające ochrony w osobne layouty lub foldery (np. (protected)/), a logikę redirectu umieścić w jednym miejscu — np. w app/(protected)/_layout.tsx. To ułatwia utrzymanie i eliminuje duplikację kodu.

Powiązane artykuły
Zobacz wszystkie
Odkryj więcej tematów