Melden Sie einen Nutzer in einer session-basierten App ab, vergisst der Server ihn bei der nächsten Anfrage. Melden Sie ihn in einer JWT-basierten App ab, bleibt das Token, das er besitzt, gültig, bis es abläuft – es sei denn, Sie haben einen zusätzlichen Mechanismus dafür gebaut. Dieser Unterschied ist die eigentliche Entscheidung hinter „JWT vs. Session". Der Rest folgt daraus.

Was eine Session tatsächlich speichert

Eine Session hält den Zustand des Nutzers auf dem Server und gibt dem Browser nur einen Schlüssel zum Nachschlagen.

  1. Der Nutzer meldet sich mit einem Passwort an.
  2. Der Server legt einen Session-Datensatz an (Nutzer-ID, Rollen, was auch immer er braucht) und speichert ihn im Speicher, in einer Datenbank oder in Redis, indiziert mit einer zufälligen Session-ID.
  3. Der Server sendet diese ID als Cookie an den Browser, markiert mit HttpOnly, damit client-seitiges JavaScript es nicht lesen kann.
  4. Bei jeder Anfrage sendet der Browser das Cookie automatisch zurück. Der Server sucht die ID im Store und holt die Session-Daten.

Das Cookie trägt keine Nutzerinformation, nur eine undurchsichtige Zeichenfolge, die der Server bei jeder Anfrage auflöst. Löschen Sie den Datensatz im Store, ist die Session bei der nächsten Anfrage verschwunden – auf jedem Gerät.

Was in einem JWT steckt

Ein JSON Web Token überspringt das Nachschlagen, indem es den Zustand direkt in das Token legt, signiert, damit es nicht manipuliert werden kann.

Ein JWT besteht aus drei base64url-kodierten Teilen, durch Punkte verbunden: header.payload.signature. Das Payload ist ein JSON-Objekt aus Claims. RFC 7519 definiert eine kurze Liste registrierter Claims:

ClaimNameBedeutung
issIssuerWer das Token ausgestellt hat
subSubjectAuf wen sich das Token bezieht
audAudienceFür wen das Token bestimmt ist
expExpirationToken nach diesem Zeitpunkt ablehnen
nbfNot beforeToken vor diesem Zeitpunkt ablehnen
iatIssued atWann das Token erstellt wurde
jtiJWT IDEindeutige ID, nützlich für Widerruf und Replay-Prüfungen

Der Server signiert Header und Payload mit einem Secret (HMAC) oder einem privaten Schlüssel (RSA/ECDSA) und sendet das Ergebnis an den Client. Jeder Server, der den passenden Schlüssel besitzt – oder bei asymmetrischer Signatur nur den öffentlichen Schlüssel –, kann die Signatur prüfen und den Claims vertrauen. Kein Datenbank-Roundtrip, kein gemeinsamer Session-Store. In einem System mit mehreren Diensten kann jeder ein Token eigenständig prüfen.

Die Kehrseite: Wer das Token besitzt, kann das Payload lesen. Base64 ist eine Kodierung, keine Verschlüsselung. Packen Sie niemals ein Passwort, ein Secret oder personenbezogene Daten, die Sie nicht in ein Log schreiben würden, in das Payload eines JWT.

Wie Sie Session-Authentifizierung in Express einrichten

Eine typische Node-Konfiguration nutzt express-session als Middleware und connect-redis, damit Sessions einen Server-Neustart überstehen und über mehrere Instanzen funktionieren. Wenn Redis für Sie neu ist, deckt wie Redis-Caching funktioniert die Grundlagen ab, um es zum Laufen zu bringen.

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 Stunden
    },
  })
);

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() entfernt den Eintrag in Redis. Die nächste Anfrage mit diesem Cookie findet nichts mehr und wird als abgemeldet behandelt. Keine Token-Blacklist, nichts, was später nachgerüstet werden muss.

Wie Sie JWT-Authentifizierung in Node einrichten

Derselbe Ablauf mit jsonwebtoken, mit einem kurzlebigen Access-Token und einem separaten 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 prüft die Signatur und die Claims exp/nbf in einem einzigen Aufruf und wirft eine Exception, wenn eines der beiden fehlschlägt. Das ist die gesamte Authentifizierungsprüfung, ohne dass ein Store beteiligt ist.

Die Option algorithms: ["HS256"] ist entscheidend. Ohne sie könnte ein Token, das mit einem anderen Algorithmus signiert wurde als vorgesehen, trotzdem durchkommen – genau so sind mehrere JWT-Bibliotheken bei „Alg Confusion"-Schwachstellen gelandet. Legen Sie die akzeptierte Algorithmenliste bei der Prüfung fest und lehnen Sie alg: "none" grundsätzlich ab.

Das Refresh-Token macht ein 15-minütiges Access-Token erst praktikabel. Der Client behält es und tauscht es, wenn das Access-Token abläuft, gegen ein neues Paar. Speichern Sie das jti des Refresh-Tokens serverseitig, damit Sie genau diese eine Kette widerrufen können, ohne bereits im Umlauf befindliche Access-Tokens anzufassen.

Warum JWTs schwer zu widerrufen sind

Eine Session ist ein Datensatz in einem Store: Löschen Sie ihn, ist der Nutzer überall abgemeldet. Ein JWT ist ein eigenständiger Claim, den jeder Prüfer akzeptiert, bis exp etwas anderes sagt. Abmelden, einen Nutzer sperren oder seine Rolle herabstufen ändert an einem bereits ausgestellten Token nichts, weil der prüfende Server keinen Datensatz hat, den er löschen könnte.

Die üblichen Gegenmaßnahmen:

  • Kurze Lebensdauer des Access-Tokens. 15-30 Minuten ist der gängige Bereich, damit ein gestohlenes oder veraltetes Token nur ein kleines Zeitfenster hat.
  • Rotation des Refresh-Tokens. Jeder Refresh stellt ein neues Refresh-Token aus und macht das alte ungültig; die Wiederverwendung eines alten Refresh-Tokens widerruft die gesamte Kette. OAuth 2.1 verlangt das (oder sender-gebundene Token) für öffentliche Clients.
  • Eine eng gefasste Denylist. Um ein bestimmtes Token vor dem Ablauf zu töten, indizieren Sie die Denylist über jti plus iss, nicht über die rohe Token-Zeichenfolge. Das OWASP JWT Cheat Sheet erklärt, warum das Blacklisten der Zeichenfolge selbst unzuverlässig ist. Eine bei jeder Anfrage geprüfte Denylist bringt auch wieder einen Datenbank-Lookup bei jeder Anfrage mit sich – genau die Kosten, die JWTs eigentlich vermeiden sollten.

Wenn Ihre App am Ende ohnehin bei jeder Prüfung ein Nachschlagen macht, wäre eine Session das einfachere Design gewesen.

Wo Sie ein JWT im Browser speichern

Legen Sie in einer Browser-App das Token in ein HttpOnly-, Secure-Cookie, nicht in localStorage. Jedes Skript, das auf der Seite läuft, kann localStorage lesen, also liefert eine einzige XSS-Lücke jedes darin enthaltene Token aus. Die Kehrseite: Sobald das Token in einem Cookie reist, sendet der Browser es automatisch, und Sie müssen sich wieder mit SameSite und CSRF-Token gegen CSRF verteidigen – genau wie bei Sessions. Der vollständige Vergleich steht in LocalStorage, SessionStorage oder Cookies: Was wofür.

Wann Sessions und wann JWTs verwenden

SituationVerwenden Sie
Eine serverseitig gerenderte App, ein BackendSessions: einfacher, sofortiger Widerruf
Sofortiger Logout oder sofortige RechteänderungenSessions, oder JWTs mit einer Denylist über jti
Mehrere Backend-Dienste, die den Nutzer jeweils eigenständig prüfenJWTs: kein gemeinsamer Session-Store, den man ansprechen muss
Mobile App oder API-Clients von DrittanbieternJWTs: Bearer-Token passen zu einer zustandslosen HTTP-API

Keines der beiden ist im Abstrakten sicherer. Eine Redis-gestützte Session und ein 15-minütiges JWT mit rotierten Refresh-Tokens halten beide stand. Der Unterschied ist architektonisch: Wie viele Dienste müssen dem Claim vertrauen, und wie schnell müssen Sie ihn zurücknehmen können.

Bauen Sie eine App mit einem Backend? Wählen Sie standardmäßig Sessions und bekommen den Widerruf kostenlos dazu. Wechseln Sie zu JWTs an dem Tag, an dem ein zweiter Dienst wissen muss, wer der Nutzer ist, ohne den Dienst zurückzurufen, der ihn angemeldet hat.

Verwandte Artikel