Startseite / Artikel / Sessions gegen JWTs: Die richtige Authentifizierungsarchitektur für Node.js auswählen

Sessions gegen JWTs: Die richtige Authentifizierungsarchitektur für Node.js auswählen

Erfahren Sie, wie Sessions und JWTs bei der Authentifizierung in Node.js tatsächlich voneinander abweichen, wie jede Methode funktioniert, und wie Sie zwischen ihnen wählen können, ohne später Bedauern zu haben.

1335 Wörter

Das Problem hinter beiden Ansätzen

HTTP verfügt nicht über ein eingebautes Speichersystem für Informationen zwischen den einzelnen Anfragen. Jede eintreffende Anfrage erscheint wie von einem völlig Unbekannten, es sei denn, sie enthält einen Nachweis der Identität. Sowohl Sessions als auch JWTs lösen dieses Problem auf dieselbe grundlegende Weise: Sie geben dem Client einen Datensatz mit, den dieser bei jeder nachfolgenden Anfrage zurücksenden kann. Was sie tatsächlich voneinander unterscheidet, ist die Art dieses Datensatzes sowie – noch wichtiger – wo sich die autoritative Aufzeichnung darüber befindet, „wer eingeloggt ist“.

Option eins: Sessions

In einer auf Sessions basierenden Konfiguration liegt die Autorität beim Server. Sobald sich ein Benutzer anmeldet, erstellt der Server einen zufälligen Session-Identifikator, speichert die tatsächlichen Benutzerdaten unter diesem Identifikator an einem Ort wie Redis oder in einer Datenbank und gibt dem Client lediglich den Identifikator selbst, der in der Regel in einer Cookie gespeichert wird.

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();
});

Von diesem Zeitpunkt an löst jede eingehende Anfrage eine Suche dort aus, wo die Sitzungsdaten gespeichert sind. Dieses einzige Mechanismus erklärt sowohl, warum Sitzungen nützlich sind, als auch, warum sie Overhead mit sich bringen.

Der Vorteil: Man hat sofortige und vollständige Kontrolle darüber, wer eingeloggt ist. Das Beenden einer Sitzung – egal ob durch das Ausloggen eines Benutzers, ein Passwort zurücksetzen oder dass ein Administrator ein kompromittiertes Konto deaktiviert – besteht lediglich aus der Löschung in dem Sitzungsspeicher. Es ist nicht notwendig, darauf zu warten, dass etwas von selbst abläuft.

Der Nachteil: Jede Anfrage erfordert nun eine Hin- und Rückreise zum Sitzungsspeicher, was zu einer geringen, aber real vorhandenen Verzögerung sowie zu einem erhöhten Infrastrukturbelastung führt. Zudem wird der Sitzungsspeicher zu einem gemeinsamen Zustand, den alle Server erreichen müssen – was bereits ab dem Zeitpunkt, an dem man über einen einzelnen Server hinaus skaliert, zu einem ernsthaften architektonischen Problem wird.

Option Zwei: JWTs

Ein JWT (JSON Web Token) funktioniert anders: Der Server signiert ein Token, das bereits die Daten des Benutzers enthält, und der Client sendet dieses genaue Token bei jeder Anfrage erneut. Der Server überprüft die Signatur und vertraut dem Inhalt, ohne etwas anderswo nachschauen zu müssen.

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();
});

Es werden weder Datenbankaufrufe noch Redis-Aufrufe benötigt. Die kryptografische Signatur allein reicht aus, um zu bestätigen, dass das Token seit seiner Ausstellung nicht manipuliert wurde.

Der Vorteil: Es gibt keine Datenbank- oder Cacheabfragen pro Anfrage, was die Geschwindigkeit erheblich erhöht, und es gibt keinen gemeinsamen Session-Speicher, auf den alle Server zugreifen müssen. Dadurch wird das horizontale Skalieren vereinfacht und es fällt leichter, statelessen Microservice-Architekturen zu verstehen.

Der Kompromiss: Sobald ein JWT ausgehändigt wurde, verfügt der Server nicht über eine eingebaute Möglichkeit, es vor Ablauf der Gültigkeitsdauer zu ungültig zu machen. Wenn das Token gestohlen wird oder der Zugriff eines Benutzers umgehend eingeschränkt werden muss, kann der Server nichts löschen, da ursprünglich nichts Zentrales gespeichert wurde. Dies ist der Kompromiss, der die Menschen am häufigsten überrascht – in der Regel erst dann, nachdem sie ihr System bereits so konzipiert haben, als wäre ein JWT lediglich eine schnellere, bessere Version einer Sitzung.

Die Fehlvorstellung, die echte Probleme verursacht

Der Ausdruck „JWTs sind stateless“ wird so oft als unqualifizierter Verkaufsargument verwendet, dass die Leute übersehen, was er tatsächlich bedeutet: Statelessness bedeutet standardmäßig auch keine Revokation. Wenn ein Session-Cookie kompromittiert wird, reicht es aus, ihn einfach zu löschen. Ein gestohlenes JWT hingegen bleibt solange es innerhalb seines Gültigkeitszeitraums ist, vollständig gültig und wird vom Server weiterhin vollständig vertraut behandelt – es sei denn, man hat extra Maßnahmen ergriffen, um das zu verhindern.

Die übliche Lösung, die die Leute anwenden, besteht darin, eine Blockliste von revokierten Tokens zu pflegen:

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();
});

Das funktioniert zwar, aber schauen Sie sich genau an, was Sie gerade getan haben: Sie haben eine pro-Anfrage durchgeführte Überprüfung gegen einen gemeinsamen Datenspeicher eingeführt, was genau die Overhead-Kosten sind, von denen JWTs eigentlich befreien sollten. An diesem Punkt haben Sie eigentlich kein stateless System mehr. Was Sie stattdessen haben, ist ein sessionsbasiertes System in Verkleidung mit einem noch unübersichtlicheren Fehlerverhalten dahinter.

Welches sollten Sie also tatsächlich verwenden?

Wählen Sie Sessions, wenn: Sie eine sofortige und zuverlässige Revokation benötigen (denken Sie an Bank-Apps, Admin-Dashboards oder alles, wo die Sicherheitsanforderungen hoch sind), Sie eine Anwendung hinter einem Load Balancer betreiben, der problemlos auf einen gemeinsamen Session-Speicher verweisen kann, oder Sie lieber eine einzige Quelle der Wahrheit pflegen möchten, anstatt sich mit Token-Lebenszyklen und Blocklist-Logik herumzuschlagen.

Nutzen Sie JWTs, wenn: Sie Authentifizierung in wirklich unabhängigen Diensten verwalten, die nicht alle direkten Zugriff auf einen gemeinsamen Session-Speicher benötigen, Sie ein System entwickeln, bei dem kurze Gültigkeitsdauern der Tokens (Minuten statt Tage) die Lücke bei der Revokation akzeptabel macht, oder der eigentliche Grund für den Zustandslosigkeitsansatz darin besteht, dass Sie viele unabhängige API-Nutzer bedienen – und nicht nur, weil dies eleganter klingt.

Eine Nuance, die erwähnt werden sollte: In der Praxis wählen die meisten Produktionsysteme nicht eindeutig eine Seite. Das Muster, auf das sie sich zubewegen, besteht aus kurzlebigen JWTs in Kombination mit einem auf der Serverseite gespeicherten Refresh-Token. Es handelt sich dabei um eine Mischung statt um eine binäre Entscheidung: Sessions kümmern sich um langfristiges Vertrauen und die Revokation, während JWTs kurze, zustandslose Überprüfungsphasen dazwischen abdecken. Wenn Sie „Session gegen JWT“ als eine Entweder-oder-Entscheidung betrachten, ist das in der Regel ein Zeichen dafür, dass Sie noch nicht die Skalenebene erreicht haben, bei der diese hybride Konfiguration ihre Komplexität rechtfertigt – was bedeutet, dass eine einfache Session wahrscheinlich der ehrlichere und einfachere Ausgangspunkt ist.

Die tatsächliche Entscheidung

Hinter all dem war die Frage nie rein technischer Natur – es geht vielmehr darum, zu entscheiden, wo die Kosten absorbiert werden. Sessions erheben diese Kosten bei jeder einzelnen Anfrage, doch im Gegenzug erhält man eine stets aktuelle Kontrolle, die niemals veraltet ist. JWTs beseitigen diese Gebühr pro Anfrage, doch der Preis dafür ist ein Zeitraum, in dem das, was der Server für wahr hält, und das, was tatsächlich zutrifft, sich leise voneinander entfernen können. Keiner dieser Ansätze verdient die Bezeichnung „modern“ oder „veraltet“, egal, wie die Diskussionen um sie geführt werden. Es handelt sich einfach um zwei verschiedene Möglichkeiten, denselben Kompromiss zu finden, der niemals wirklich verschwindet.

Verwandte Artikel

  • Wählen zwischen EC2, ECS und EKS für Node.js-Arbeitslasten — Vergleicht, wie EC2, ECS mit Fargate und EKS die Betriebstechnik von Node.js-Anwendungen handhaben, um Ihnen dabei zu helfen, den richtigen AWS-Rechendienst für die Größe und Fähigkeiten Ihres Teams auszuwählen.
  • Implementierung von Zugriffs- und Auffrischungstokens zusammen in Node.js — Lernen Sie, wie Sie kurze Zugriffs-Token mit rotierenden Auffrischungstokens in Node.js kombinieren können, um Sicherheit und reibungslose Benutzersitzungen in Einklang zu bringen.
  • Erstellung einer für die Produktion geeigneten JWT-Autentifizierung in Node.js-APIs — Wie man Passwörter hasht, kurzlebige JWTs ausstellt, Refresh-Tokens sowie Mechanismen zur Revokation hinzufügt, Autorisierung von Berechtigungen trennt, Angriffe durch Brute Force abwehrt und überprüft, ob die Autentifizierung korrekt fehlschlägt.
  • Wann JWT-Autentifizierung stateful wird: Ein Fall für Server-Seitige Sessions in Node — Erfahren Sie, wie Revokations-Blocklisten und Refresh-Speicher die JWT-Autentifizierung anfällig machen, wie Express-Sessions mit Postgres diese vereinfachen und wo JWTs weiterhin geeignet sind.