Weg A oder Weg B — JWT oder Session?
Die Architekturentscheidung: JWT oder Session für die neue API?
Einleitung
Du baust eine API und musst dich für eine Authentifizierungsstrategie entscheiden. Zwei Wege tun sich auf: JWT-Tokens, die der Client bei jedem Request mit sich führt, oder klassische Sessions, die serverseitig gehalten werden. Beides funktioniert. Beides hat seine Daseinsberechtigung. Aber für welches Projekt passt was?
Ich musste diese Entscheidung kürzlich für ein B2B-Portal treffen, das sowohl eine Web-App als auch mobile Clients bedient. Die Antwort klingt nach Glaubenskrieg, ist aber oft eine Frage nach konkretem Betriebsaufwand und Skalierungsplan.
Kontext
Es geht um ein Portal, das von internen Mitarbeitern und externen Partnern genutzt wird. Die API soll sowohl von einem React-Frontend als auch von einer nativen iOS- und Android-App angesprochen werden. Der Traffic ist überschaubar — im Schnitt 500 Requests pro Minute, mit Spitzen bei Quartalsabschlüssen. Das Team besteht aus drei Entwicklern, die mit Node.js und Go vertraut sind, aber keine langjährige Erfahrung mit Identity-Providern oder Token-Revocation haben.
Was auf dem Spiel steht: Einfache Wartung, schnelle Debugging-Möglichkeiten, transparente Logins. Non-Goals: Single-Sign-On für zehntausend externe Nutzer, komplexe OAuth2-Flows oder Multi-Faktor-Authentifizierung auf Token-Ebene.
Option A: JWT
JSON Web Tokens sind der moderne Standard, wenn es um zustandslose Authentifizierung geht. Der Server signiert ein Token, der Client sendet es bei jedem Request mit — fertig.
Pros:
- Zustandslos: Der Server muss keine Session speichern, das skaliert ohne Shared Storage
- Funktioniert über Domains und Services hinweg, solange das Token gültig ist
- Einfach in Mobile-Apps und SPAs zu integrieren — kein Cookie-Handling nötig
Cons:
- Revocation ist schwierig: Ein gestohlenes Token bleibt gültig bis zum Ablauf — es sei denn, du baust eine Blacklist
- Tokens können groß werden: Bei vielen Claims wird der Header unhandlich
- Debugging ist aufwändiger: Du siehst im Server-Log nur, dass das Token ungültig ist — nicht warum
// Beispiel: JWT Payload
{
"sub": "user-42",
"name": "user-42",
"role": "architect",
"exp": 1755438000,
"iat": 1755434400
}
Option B: Session
Klassische Sessions speichern den Zustand serverseitig und geben dem Client nur ein Cookie mit einer Session-ID zurück. Der Server prüft bei jedem Request, ob die ID gültig ist.
Pros:
- Sofortige Revocation: Ein
DELETE FROM sessions WHERE id = ?beendet den Login sofort - Kleine Cookies: Nur eine Session-ID, kein Payload-Chaos
- Einfache Debugging-Möglichkeiten: Du kannst Sessions im Admin-Interface einsehen, verwalten und ungültig schreiben
Cons:
- Zustandsbehaftet: Bei mehreren Servern brauchst du Shared Storage wie Redis oder eine Datenbank
- CORS-Aufwand bei Cross-Domain-APIs: Cookies brauchen
SameSite-Attribute und Domain-Konfiguration - Skalierung erfordert Infrastruktur: Ohne Redis wird es bei mehr als einem Replica eng
# Beispiel: Session-Config in Express
session:
secret: "${SESSION_SECRET}"
resave: false
saveUninitialized: false
cookie:
httpOnly: true
secure: true
sameSite: strict
maxAge: 3600000 # 1 Stunde
store: redis # Benötigt Redis für Multi-Replica-Setup
Entscheidung
Ich habe mich für Sessions entschieden — zumindest für dieses Portal.
Die Begründung ist betriebsorientiert: Wir brauchen die Möglichkeit, Nutzer sofort auszusperren, wenn ein Gerät verloren geht oder ein Account kompromittiert wird. Mit Sessions ist das ein einzelnes DEL in Redis. Mit JWT bräuchten wir eine Blacklist, die genauso komplex ist wie ein Session-Store — und trotzdem an Token-Ablaufzeiten gebunden bleibt.
Der Trade-off: Wir müssen Redis für den Session-Store betreiben und CORS-Konfigurationen pflegen. Ich akzeptiere das zugunsten der schnellen Revocation und der einfacheren Fehleranalyse im Support-Fall.
Implementierung
Der Einstieg ist ein einzelnes Redis-Volume plus ein Session-Middleware:
# docker-compose.yml
services:
api:
image: portal-api:latest
environment:
- SESSION_SECRET=${SESSION_SECRET}
- REDIS_URL=redis://redis:6379
depends_on:
- redis
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
volumes:
redis-data:
In der Anwendung reicht ein Middleware-Aufruf:
// Beispiel: Session-Middleware in Go mit gorilla/sessions
import "github.com/gorilla/sessions"
var store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET")))
store.Options = &sessions.Options{
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
MaxAge: 3600,
}
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "portal-session")
if session.Values["user_id"] == nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
Für das Logout reicht ein einzelnes delete:
func Logout(w http.ResponseWriter, r *http.Request) {
session, _ := store.Get(r, "portal-session")
session.Options.MaxAge = -1
session.Save(r, w)
}
Fazit
Sessions sind die bessere Wahl, wenn du sofortige Revocation brauchst und die Komplexität von Token-Blacklists vermeiden willst. JWT bleibt die richtige Entscheidung, wenn du wirklich zustandslos skalieren musst — also bei tausenden parallelen Instanzen ohne Shared Storage oder bei echten Multi-Domain-Setups.
Die Metrik, die für uns zählt: Die durchschnittliche Zeit vom gemeldeten Sicherheitsvorfall bis zur Sperrung des Accounts ging von unter fünf Minuten auf unter zehn Sekunden runter — weil wir nicht auf Token-Ablaufzeiten warten müssen, sondern sofort löschen können.
Wie geht ihr mit Revocation in euren APIs um? Ich bin neugierig, ob jemand mit gemischten Ansätzen — also JWT für Public-APIs und Sessions für interne Bereiche — Erfahrung hat.
Siehe auch
[Weg A oder Weg B — Redis oder Memcache?](/blog/redis-oder-memcache-eine-architekturentscheidung/)[Nginx oder Caddy? Eine Architekturentscheidung](/blog/nginx-oder-caddy-eine-architekturentscheidung/)[Weg A oder Weg B — Docker Compose oder Kubernetes?](/blog/weg-a-oder-weg-b-docker-compose-oder-kubernetes/)