Strona główna / Artykuły / Sesje kontra JWT: wybór odpowiedniego modelu autoryzacji w Node.js

Sesje kontra JWT: wybór odpowiedniego modelu autoryzacji w Node.js

Dowiedz się, w jaki sposób sesje i JWT różnią się w mechanizmach autoryzacji w Node.js, jak działają każde z nich oraz jak wybrać między nimi, aby później nie żałować tej decyzji.

1335 słów

Problem wspólny obu podejściom

HTTP nie posiada wbudowanej pamięci o żadnych informacjach z jednego żądania do następnego. Każde przychodzące żądanie wygląda tak, jakby pochodziło od zupełnie obcego użytkownika, chyba że zawiera ze sobą dowód tożsamości. Zarówno sesje, jak i JWT rozwiązują ten problem w ten sam podstawowy sposób: przekazują klientowi fragment danych, który musi zostać zwrócony przy każdym kolejnym żądaniu. To, co faktycznie je różni, to charakter tych danych oraz, co ważniejsze, miejsce przechowywania autorytatywnego zapisu o tym, „kto jest zalogowany”.

Opcja pierwsza: sesje

W rozwiązaniu opartym na sesjach władzę ma serwer. Gdy użytkownik się loguje, serwer tworzy losowy identyfikator sesji, przechowuje rzeczywiste informacje o użytkowniku w związku z tym identyfikatorem gdzieś takim jak Redis lub baza danych, a klient otrzymuje jedynie sam identyfikator, zwykle przechowywany w pliku cookie.

app.post("/login", async (req, res) => {
  const user = await authenticate(req.body.email, req.body.password);
  const sessionId = generateSecureId();

  await redis.set(`session:${sessionId}`, JSON.stringify({ userId: user.id }), "EX", 86400);
  res.cookie("sessionId", sessionId, { httpOnly: true, secure: true });
  res.json({ success: true });
});

app.use(async (req, res, next) => {
  const sessionId = req.cookies.sessionId;
  const session = await redis.get(`session:${sessionId}`);
  req.user = session ? JSON.parse(session).userId : null;
  next();
});

Od tego momentu każda przychodząca prośba wywołuje wyszukiwanie w miejscu, gdzie przechowywane są dane sesji. Ten jeden mechanizm wyjaśnia zarówno, dlaczego sesje są przydatne, jak i dlaczego generują dodatkowe obciążenie.

Zalety: masz natychmiastową i pełną kontrolę nad tym, kto pozostaje zalogowany. Zakończenie sesji, niezależnie od tego, czy jest spowodowane wylogowaniem się użytkownika, sprostowaniem hasła, czy zamknięciem konta przez administratora, polega po prostu na usunięciu wpisu w magazynie sesji. Nie trzeba czekać, aż coś wygaśnie samo z siebie.

Koszty: każda prośba wymaga teraz dwukierunkowej komunikacji z magazynem sesji, co dodaje niewielki, ale rzeczywisty opóźnienie oraz obciążenie infrastrukturą. Ponadto magazyn sesji staje się wspólnym stanem, do którego muszą mieć dostęp wszystkie serwery, co staje się poważnym problemem architektonicznym w momencie, gdy skalujesz system poza jeden serwer.

Opcja druga: JWT

JWT (JSON Web Token) działa inaczej: serwer podpisuje token, który już zawiera dane użytkownika, a klient przesyła dokładnie ten sam token przy każdej prośbie. Serwer sprawdza podpis i ufa zawartym w nim informacjom, nie musząc niczego szukać gdzie indziej.

app.post("/login", async (req, res) => {
  const user = await authenticate(req.body.email, req.body.password);
  const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: "1h" });
  res.cookie("token", token, { httpOnly: true, secure: true });
  res.json({ success: true });
});


app.use((req, res, next) => {
  try {
    const decoded = jwt.verify(req.cookies.token, process.env.JWT_SECRET);
    req.user = decoded.userId;
  } catch {
    req.user = null;
  }
  next();
});

Nie ma żadnych wywołań bazy danych ani Redis. Sam podpis kryptograficzny wystarcza, aby potwierdzić, że token nie został zmodyfikowany od chwili jego wydania.

Zalety: nie ma konieczności wyszukiwania danych w bazie lub pamięci podręcznej przy każdej prośbie, co znacznie przyspiesza działanie, a także nie ma wspólnej pamięci sesji, do której musiałyby uzyskać dostęp wszystkie serwery – to upraszcza skalowanie poziome i sprawia, że architektury mikrosług bez stanu są znacznie łatwiejsze do zrozumienia.

Kompromis: gdy token JWT zostanie już wydany, serwer nie ma żadnego wbudowanego sposobu na unieważnienie go przed upływem terminu ważności. Jeśli token zostanie skradziony lub jeśli konieczne jest natychmiastowe odcięcie dostępu użytkownika, serwer nie może nic usunąć, ponieważ od początku nie przechowywano żadnych danych centralnie. To właśnie ten kompromis najczęściej zaskakuje ludzi, zwykle po tym, jak już zaprojektowali swój system w oparciu o założenie, że JWT to po prostu szybsza i lepsza wersja sesji.

Błędne przekonanie powodujące prawdziwe problemy

Wyrażenie „JWTs są bezstanowe” jest tak często używane jako bezkrytyczna zaleta, że ludzie przeoczają to, co ono faktycznie oznacza: brak stanu oznacza również domyślnie brak możliwości anulowania. Jeśli plik cookie sesji zostanie skompromitowany, wystarczy go usunąć. Natomiast skradzione JWT pozostaje w pełni ważne i jest w pełni zaufane przez serwer przez cały czas trwania okna ważności, chyba że specjalnie zaimplementowano coś dodatkowego, aby temu zapobiec.

Typowym rozwiązaniem, do którego uciekają się ludzie, jest utrzymywanie listy blokad tokenów anulowanych:

app.use(async (req, res, next) => {
  try {
    const decoded = jwt.verify(req.cookies.token, process.env.JWT_SECRET);
    const isRevoked = await redis.get(`revoked:${decoded.jti}`);
    if (isRevoked) throw new Error("Token revoked");
    req.user = decoded.userId;
  } catch {
    req.user = null;
  }
  next();
});

To faktycznie działa, ale przyjrzyj się uważnie temu, co właśnie zrobiłeś: przywróciłeś sprawdzanie na poziomie każdej prośby wobec wspólnego magazynu danych, co jest dokładnie tym obciążeniem, od którego miały uwolnić JWT. W tym momencie już nie masz systemu bezstanowego. Masz raczej system oparty na sesjach w przebraniu, z bardziej skomplikowanym modelem radzenia sobie z błędami.

Który więc powinieneś faktycznie używać?

Wybierz sesje, gdy: potrzebujesz natychmiastowej i niezawodnej odwołania uprawnień (myśl o aplikacjach bankowych, panelach administracyjnych lub czymkolwiek, gdzie bezpieczeństwo ma duże znaczenie), uruchamiasz jedną aplikację za load balancerem, który może bez większych problemów wskazywać na wspólny magazyn sesji, albo po prostu wolisz utrzymywać jedyny źródło prawdy zamiast zajmować się czasem życia tokenów i logiką list blokujących.

Korzystaj z JWT, gdy: obsługujesz autoryzację w naprawdę niezależnych usługach, które nie powinny wszystkie wymagać bezpośredniego dostępu do jednego wspólnego magazynu sesji, budujesz system, w którym krótki czas życia tokenów (minuty zamiast dni) sprawia, że przerwa w odwołaniu staje się akceptowalna, lub rzeczywistym powodem chęci utrzymania braku stanu jest obsługa wielu niezależnych użytkowników API, a nie tylko to, że brzmi to lepiej.

Jedna nuans, na którą warto zwrócić uwagę: w praktyce większość systemów produkcyjnych nie wybiera jednej z tych opcji w sposób ostateczny. Wzorzec, do którego się przyzwyczajają, to krótkotrwałe tokeny JWT połączone z tokenem odnowienia przechowywanym po stronie serwera. Jest to mieszanka, a nie rozwiązanie binarne: sesje zarządzają długoterminowym zaufaniem i anulowaniem dostępu, natomiast tokeny JWT służą do krótkotrwałej, bezstanowej weryfikacji pomiędzy nimi. Jeśli traktujesz kwestię „sesja kontra JWT” jako decyzję typu albo-to-albo, to zazwyczaj oznacza to, że jeszcze nie osiągnąłeś takiego poziomu skali, przy którym to hybrydowe rozwiązanie uzasadnia swoją złożoność, co oznacza, że zwykła sesja jest prawdopodobnie uczciwszym i prostszym punktem wyjścia.

Rzeczywista decyzja

W gruncie rzeczy pytanie to nigdy nie było czysto techniczne – chodzi tu o decyzję, gdzie zostaną pokryte koszty. Sesje naliczają te koszty za każdą prośbę, ale w zamian oferują kontrolę, która jest zawsze aktualna i nigdy przestarzała. JWT eliminują te opłaty za każdą prośbę, ale ceną jest okres czasu, w którym to, w co wierzy serwer, i to, co faktycznie jest prawdą, mogą się cicho rozchodzić. Żaden z tych podejść nie zasługuje na miano „nowoczesnego” czy „przestarzałego”, bez względu na to, jak kształtuje się dyskusja na ich temat. Są to po prostu dwa różne sposoby rozwiązania tego samego kompromisu, który w rzeczywistości nigdy nie znika.

Literatura pokrewna

  • Wybór między EC2, ECS a EKS dla zadań Node.js — Porównuje sposób, w jaki EC2, ECS z Fargate oraz EKS zarządzają aplikacjami Node.js pod względem operacyjnym, pomagając wybrać odpowiednią usługę obliczeniową AWS dostosowaną do skali i umiejętności zespołu.
  • Wdrożenie tokenów dostępu i odnowienia jednocześnie w Node.js — Dowiedz się, jak łączyć krótkotrwałe tokeny dostępu z rotującymi tokenami odnowienia w Node.js, aby zapewnić równowagę pomiędzy bezpieczeństwem a płynnymi sesjami użytkowników.
  • Budowanie autoryzacji JWT przygotowanej do użycia w produkcji w API Node.js — Jak haszować hasła, wydawać krótkotrwałe tokeny JWT, dodawać tokeny odnowienia i mechanizmy anulowania, oddzielać uprawnienia, chronić przed atakami siłowymi oraz sprawdzać, czy autoryzacja działa poprawnie w przypadku błędów.
  • Kiedy autoryzacja JWT staje się zależna od stanu: przykład użycia sesji po stronie serwera w Node — Zobacz, jak listy blokujące i magazyny tokenów odnowienia czynią autoryzację JWT kruchą, jak sesje w Express z bazą danych Postgres je upraszczają oraz gdzie JWT nadal mają zastosowanie.