Wyloguj użytkownika z aplikacji opartej na sesjach, a serwer zapomni o nim przy następnym żądaniu. Wyloguj go z aplikacji opartej na JWT i, jeśli nie zbudowałeś dodatkowego mechanizmu, token, który trzyma, zostaje ważny aż do wygaśnięcia. Ta różnica jest prawdziwą decyzją stojącą za “JWT vs sesja”. Reszta z niej wynika.
Co faktycznie przechowuje sesja
Sesja trzyma stan użytkownika na serwerze, a przeglądarce przekazuje tylko klucz do wyszukania.
- Użytkownik loguje się hasłem.
- Serwer tworzy rekord sesji (ID użytkownika, role, co tylko potrzebne) i zapisuje go w pamięci, w bazie danych albo w Redis, indeksowany losowym ID sesji.
- Serwer wysyła ten ID do przeglądarki jako cookie, oznaczone
HttpOnly, żeby JavaScript po stronie klienta nie mógł go odczytać. - Przy każdym żądaniu przeglądarka automatycznie odsyła cookie. Serwer szuka ID w store i pobiera dane sesji.
Cookie nie niesie żadnej informacji o użytkowniku, tylko nieprzejrzysty ciąg, który serwer rozwiązuje przy każdym żądaniu. Usuń rekord w store i sesja znika przy następnym żądaniu, na każdym urządzeniu.
Co jest wewnątrz JWT
JSON Web Token pomija to wyszukiwanie, wkładając stan prosto do tokenu, podpisanego tak, żeby nie można go było zmanipulować.
JWT to trzy części kodowane w base64url, połączone punktami: header.payload.signature. Payload to obiekt JSON z claimami. RFC 7519 definiuje krótką listę zarejestrowanych claimów:
| Claim | Nazwa | Znaczenie |
|---|---|---|
iss | Issuer | Kto wydał token |
sub | Subject | Kogo dotyczy token |
aud | Audience | Dla kogo token jest przeznaczony |
exp | Expiration | Odrzuć token po tym momencie |
nbf | Not before | Odrzuć token przed tym momentem |
iat | Issued at | Kiedy token został utworzony |
jti | JWT ID | Unikalne ID, przydatne do unieważniania i wykrywania replay |
Serwer podpisuje header i payload secretem (HMAC) albo kluczem prywatnym (RSA/ECDSA) i wysyła wynik do klienta. Każdy serwer mający odpowiadający klucz, albo tylko klucz publiczny przy podpisie asymetrycznym, może zweryfikować podpis i zaufać claimom. Żadnego roundtripu do bazy danych, żadnego wspólnego store sesji. W systemie z kilkoma serwisami każdy może zweryfikować token samodzielnie.
Druga strona medalu: kto ma token, może odczytać payload. Base64 to kodowanie, nie szyfrowanie. Nigdy nie wkładaj hasła, secretu albo danych osobowych, których nie wydrukowałbyś w logu, do payloadu JWT.
Jak skonfigurować autentykację sesyjną w Express
Typowa konfiguracja w Node używa express-session jako middleware i connect-redis, żeby sesje przetrwały restart serwera i działały na wielu instancjach. Jeśli Redis jest dla Ciebie nowy, jak działa cache Redis pokrywa podstawy, jak go uruchomić.
| |
| |
req.session.destroy() usuwa wpis w Redis. Następne żądanie z tym cookie nie znajduje nic i jest traktowane jako wylogowanie. Żadnej blacklisty tokenów, niczego do poprawiania później.
Jak skonfigurować autentykację JWT w Node
Ten sam przepływ z jsonwebtoken, wydając krótko żyjący access token i osobny refresh token:
| |
| |
jwt.verify sprawdza podpis i claimy exp/nbf w jednym wywołaniu i rzuca wyjątek, jeśli jedno z nich zawiedzie. To cała weryfikacja autentykacji, bez żadnego store w środku.
Opcja algorithms: ["HS256"] ma znaczenie. Bez niej token podpisany innym algorytmem niż zamierzony mógłby i tak przejść — tak kilka bibliotek JWT skończyło z podatnościami typu “alg confusion”. Zahardkoduj listę akceptowanych algorytmów przy weryfikacji i zawsze odrzucaj alg: "none".
Refresh token jest tym, co czyni 15-minutowy access token praktycznym. Klient go przechowuje i, gdy access token wygasa, wymienia na nową parę. Zapisz jti refresh tokenu po stronie serwera, żeby móc unieważnić tylko ten jeden łańcuch bez dotykania access tokenów już w obiegu.
Czemu JWT jest trudno unieważnić
Sesja to rekord w store: usuń go i użytkownik jest wylogowany wszędzie. JWT to samowystarczalny claim, który każdy weryfikator akceptuje, dopóki exp nie powie czegoś innego. Wylogowanie, zbanowanie użytkownika albo obniżenie jego roli nie zmienia niczego w już wydanym tokenie, bo serwer weryfikujący nie ma żadnego rekordu do usunięcia.
Typowe sposoby ograniczania tego problemu:
- Krótki czas życia access tokenu. 15-30 minut to najczęstszy zakres, żeby ukradziony albo stary token miał małe okno.
- Rotacja refresh tokenu. Każdy refresh wydaje nowy refresh token i unieważnia stary; ponowne użycie starego refresh tokenu unieważnia cały łańcuch. OAuth 2.1 tego wymaga (albo tokenów sender-constrained) dla publicznych klientów.
- Wąsko zakrojona denylist. Żeby zabić konkretny token przed wygaśnięciem, indeksuj denylist po
jtiplusiss, nie po surowym ciągu tokenu. OWASP JWT Cheat Sheet wyjaśnia, czemu blacklistowanie samego ciągu nie jest wiarygodne. Denylist sprawdzana przy każdym żądaniu też wraca z zapytaniem do bazy danych przy każdym żądaniu — dokładnie tym kosztem, który JWT miał usunąć.
Jeśli Twoja aplikacja i tak musi odpytywać bazę przy każdej weryfikacji, sesja byłaby prostszym projektem.
Gdzie przechowywać JWT w przeglądarce
W aplikacji przeglądarkowej umieść token w cookie HttpOnly i Secure, nie w localStorage. Każdy skrypt działający na stronie może odczytać localStorage, więc jedna dziura XSS wydaje wszystkie tokeny, które tam są. Koszt: gdy token jedzie w cookie, przeglądarka wysyła go automatycznie, i wracasz do obrony przed CSRF za pomocą SameSite i tokenów CSRF, dokładnie jak w sesjach. Pełne porównanie jest w LocalStorage vs SessionStorage vs Cookies: co wybrać.
Kiedy używać sesji, a kiedy JWT
| Sytuacja | Użyj |
|---|---|
| Jedna aplikacja renderowana po stronie serwera, jeden backend | Sesji: prostsze, natychmiastowe unieważnianie |
| Natychmiastowe wylogowanie albo natychmiastowe zmiany uprawnień | Sesji, albo JWT z denylistą po jti |
| Kilka serwisów backendowych, które każdy weryfikuje użytkownika samodzielnie | JWT: żadnego wspólnego store sesji do odpytywania |
| Aplikacja mobilna albo klienci API stron trzecich | JWT: bearer tokeny pasują do bezstanowego API HTTP |
Żadne z nich nie jest bezpieczniejsze w oderwaniu od kontekstu. Sesja na Redis i 15-minutowy JWT z rotowanymi refresh tokenami trzymają się równie dobrze. Różnica jest architektoniczna: ile serwisów musi zaufać claimowi i jak szybko musisz móc go odebrać.
Budujesz aplikację z jednym backendem? Wybierz sesje domyślnie i dostaniesz unieważnianie za darmo. Przejdź na JWT w dniu, w którym drugi serwis musi wiedzieć, kim jest użytkownik, bez odpytywania serwisu, który go zalogował.