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.

  1. Użytkownik loguje się hasłem.
  2. 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.
  3. Serwer wysyła ten ID do przeglądarki jako cookie, oznaczone HttpOnly, żeby JavaScript po stronie klienta nie mógł go odczytać.
  4. 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:

ClaimNazwaZnaczenie
issIssuerKto wydał token
subSubjectKogo dotyczy token
audAudienceDla kogo token jest przeznaczony
expExpirationOdrzuć token po tym momencie
nbfNot beforeOdrzuć token przed tym momentem
iatIssued atKiedy token został utworzony
jtiJWT IDUnikalne 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ć.

1
npm install express express-session connect-redis redis
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const express = require("express");
const session = require("express-session");
const { RedisStore } = require("connect-redis");
const { createClient } = require("redis");

const redisClient = createClient({ url: "redis://localhost:6379" });
redisClient.connect().catch(console.error);

const app = express();
app.use(express.json());

app.use(
  session({
    store: new RedisStore({ client: redisClient, prefix: "sess:" }),
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: {
      httpOnly: true,
      secure: true,
      sameSite: "lax",
      maxAge: 1000 * 60 * 60 * 2, // 2 godziny
    },
  })
);

app.post("/login", async (req, res) => {
  const user = await authenticate(req.body.email, req.body.password);
  req.session.userId = user.id;
  res.sendStatus(204);
});

app.get("/me", (req, res) => {
  if (!req.session.userId) return res.sendStatus(401);
  res.json({ userId: req.session.userId });
});

app.post("/logout", (req, res) => {
  req.session.destroy(() => res.sendStatus(204));
});

app.listen(3000);

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:

1
npm install express jsonwebtoken
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const crypto = require("crypto");
const express = require("express");
const jwt = require("jsonwebtoken");

const app = express();
app.use(express.json());

function issueTokens(user) {
  const accessToken = jwt.sign(
    { sub: user.id, role: user.role },
    process.env.JWT_ACCESS_SECRET,
    { expiresIn: "15m", algorithm: "HS256" }
  );
  const refreshToken = jwt.sign(
    { sub: user.id, jti: crypto.randomUUID() },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: "7d", algorithm: "HS256" }
  );
  return { accessToken, refreshToken };
}

function requireAuth(req, res, next) {
  const header = req.headers.authorization || "";
  const token = header.startsWith("Bearer ") ? header.slice(7) : null;
  if (!token) return res.sendStatus(401);

  try {
    req.user = jwt.verify(token, process.env.JWT_ACCESS_SECRET, {
      algorithms: ["HS256"],
    });
    next();
  } catch (err) {
    return res.sendStatus(401);
  }
}

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

app.get("/me", requireAuth, (req, res) => {
  res.json({ userId: req.user.sub, role: req.user.role });
});

app.listen(3000);

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 jti plus iss, 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

SytuacjaUżyj
Jedna aplikacja renderowana po stronie serwera, jeden backendSesji: 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 samodzielnieJWT: żadnego wspólnego store sesji do odpytywania
Aplikacja mobilna albo klienci API stron trzecichJWT: 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ł.

Powiązane artykuły