Wybierz sesje gdy
- Tradycyjna aplikacja webowa (SSR)
- Potrzebujesz natychmiastowego wylogowania
- Masz jeden serwer lub sticky sessions
- Nie potrzebujesz cross-domain
- Priorytetem jest bezpieczeństwo
Autentykacja użytkowników w aplikacjach webowych może być realizowana na dwa fundamentalnie różne sposoby: przez sesje serwerowe (stateful) lub tokeny (stateless). Każde podejscie ma swoje zalety, wady i scenariusze, w których sprawdza się najlepiej.
Zrozumienie różnic pomaga wybrać odpowiednie rozwiązanie dla konkretnego projektu oraz unikac typowych błędów bezpieczeństwa.
Sesja to mechanizm, w którym serwer przechowuje stan uwierzytelnienia użytkownika. Po zalogowaniu serwer tworzy rekord sesji (w pamieci, bazie danych lub Redis), a klient otrzymuje tylko identyfikator sesji (session ID) przechowywany w cookie.
Cechy:
Token (najczesciej JWT) to samodzielny nosnik informacji. Zawiera zaszyfrowane/podpisane dane użytkownika i jest przesyłany przy każdym zadaniu. Serwer nie musi przechowywac zadnego stanu - wszystko jest w tokenie.
Cechy:
┌────────┐ ┌────────┐│ Klient │ │ Serwer │└───┬────┘ └───┬────┘ │ │ │ POST /login {email, hasło} │ │ ──────────────────────────────────>│ │ │ ┌─────────────┐ │ │──│ Tworzenie │ │ │ │ sesji w DB │ │ │ └─────────────┘ │ Set-Cookie: session_id=abc123 │ │ <──────────────────────────────────│ │ │ │ GET /api/profile │ │ Cookie: session_id=abc123 │ │ ──────────────────────────────────>│ │ │ ┌─────────────┐ │ │──│ Szukanie │ │ │ │ sesji w DB │ │ │ └─────────────┘ │ {user: {...}} │ │ <──────────────────────────────────│┌────────┐ ┌────────┐│ Klient │ │ Serwer │└───┬────┘ └───┬────┘ │ │ │ POST /login {email, hasło} │ │ ──────────────────────────────────>│ │ │ ┌─────────────┐ │ │──│ Generowanie │ │ │ │ JWT │ │ │ └─────────────┘ │ {token: "eyJhbG..."} │ │ <──────────────────────────────────│ │ │ │ GET /api/profile │ │ Authorization: Bearer eyJhbG... │ │ ──────────────────────────────────>│ │ │ ┌─────────────┐ │ │──│ Weryfikacja │ │ │ │ podpisu │ │ │ └─────────────┘ │ {user: {...}} │ │ <──────────────────────────────────│import express from 'express';import session from 'express-session';import RedisStore from 'connect-redis';import { createClient } from 'redis';
const redisClient = createClient();const app = express();
// Konfiguracja sesjiapp.use(session({ store: new RedisStore({ client: redisClient }), secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { httpOnly: true, // Niedostepne dla JavaScript secure: true, // Tylko HTTPS sameSite: 'strict', // Ochrona przed CSRF maxAge: 24 * 60 * 60 * 1000 // 24 godziny }}));
// Logowanieapp.post('/login', async (req, res) => { const user = await authenticateUser(req.body); if (user) { req.session.userId = user.id; req.session.role = user.role; res.json({ success: true }); } else { res.status(401).json({ error: 'Nieprawidłowe dane' }); }});
// Chroniony endpointapp.get('/api/profile', (req, res) => { if (!req.session.userId) { return res.status(401).json({ error: 'Niezalogowany' }); } // req.session zawiera dane użytkownika res.json({ userId: req.session.userId });});
// Wylogowanieapp.post('/logout', (req, res) => { req.session.destroy(); // Usuwa sesje z Redis res.clearCookie('connect.sid'); res.json({ success: true });});import express from 'express';import jwt from 'jsonwebtoken';
const app = express();const JWT_SECRET = process.env.JWT_SECRET;
// Logowanieapp.post('/login', async (req, res) => { const user = await authenticateUser(req.body); if (user) { const token = jwt.sign( { sub: user.id, role: user.role }, JWT_SECRET, { expiresIn: '1h' } ); res.json({ token }); } else { res.status(401).json({ error: 'Nieprawidłowe dane' }); }});
// Middleware weryfikacji tokenafunction authMiddleware(req, res, next) { const authHeader = req.headers.authorization; if (!authHeader?.startsWith('Bearer ')) { return res.status(401).json({ error: 'Brak tokena' }); }
try { const token = authHeader.split(' ')[1]; const payload = jwt.verify(token, JWT_SECRET); req.user = payload; next(); } catch (error) { res.status(401).json({ error: 'Nieprawidłowy token' }); }}
// Chroniony endpointapp.get('/api/profile', authMiddleware, (req, res) => { // req.user zawiera dane z tokena res.json({ userId: req.user.sub });});
// Wylogowanie - klient po prostu usuwa token// Serwer nie musi nic robić (stateless)| Aspekt | Sesja (Stateful) | Token (Stateless) |
|---|---|---|
| Przechowywanie danych | Serwer (DB/Redis) | Klient (token) |
| Skalowalnosc | Wymaga współdzielonego storage | Łatwa - każdy serwer weryfikuje |
| Wylogowanie | Natychmiastowe (usuniecie sesji) | Trudne (token ważny do exp) |
| Rozmiar danych | Session ID: ~32 bajty | JWT: ~500+ bajtow |
| Wydajnosc | Wymaga zapytan do DB/Redis | Tylko weryfikacja podpisu |
| Cross-domain | Problemy z cookies | Łatwe (Bearer token) |
| Mobile apps | Trudniejsze (cookies) | Naturalne (nagłówek) |
| Bezpieczeństwo CSRF | Wymaga tokenow CSRF | Odporne (nie używa cookies) |
| Bezpieczeństwo XSS | Bezpieczniejsze (httpOnly) | localStorage podatne na XSS |
Wybierz sesje gdy
Wybierz tokeny gdy
Wiele nowoczesnych aplikacji łączy oba podejscia:
// Access token (krótki, stateless) + Refresh token (długi, w DB)interface TokenPair { accessToken: string; // JWT, 15 min, stateless refreshToken: string; // UUID, 7 dni, w bazie danych}
// Refresh token można unieważnić (usunąć z bazy)// Access token wygasa szybko, wiec skutek jest szybki