JWT-Authentifizierung: Wie JSON Web Tokens funktionieren
· 12 Min. Lesezeit
Inhaltsverzeichnis
- Was ist ein JSON Web Token?
- JWT-Struktur erklärt
- Wie JWT-Authentifizierung funktioniert
- Access Tokens vs. Refresh Tokens
- Node.js-Implementierungsleitfaden
- Wo JWTs sicher gespeichert werden
- Best Practices für Sicherheit
- Häufige JWT-Schwachstellen
- Den richtigen Algorithmus wählen
- JWT in verteilten Systemen
- Häufig gestellte Fragen
- Verwandte Artikel
Was ist ein JSON Web Token?
Ein JSON Web Token (JWT) ist eine kompakte, URL-sichere Methode, um Ansprüche zwischen zwei Parteien darzustellen. JWTs sind zum De-facto-Standard für Authentifizierung und Autorisierung in modernen Webanwendungen, APIs und Microservices-Architekturen geworden.
Wenn sich ein Benutzer anmeldet, generiert der Server ein JWT mit Benutzerinformationen und sendet es an den Client. Der Client fügt dieses Token dann in nachfolgende Anfragen ein, um seine Identität nachzuweisen. Stellen Sie es sich wie einen digitalen Reisepass vor, der Ihre Anmeldedaten enthält und verifiziert werden kann, ohne jedes Mal bei der ausstellenden Behörde nachzufragen.
Im Gegensatz zur traditionellen sitzungsbasierten Authentifizierung, bei der der Server den Sitzungsstatus im Speicher oder in einer Datenbank verwaltet, sind JWTs zustandslos. Alle Informationen, die zur Verifizierung des Tokens benötigt werden, sind im Token selbst enthalten. Diese architektonische Entscheidung bringt mehrere Vorteile:
- Skalierbarkeit: Kein Bedarf an Sitzungsspeicher oder Sticky Sessions in lastverteilten Umgebungen
- Domänenübergreifende Authentifizierung: JWTs funktionieren nahtlos über verschiedene Domänen und Dienste hinweg
- Mobilfreundlich: Perfekt für mobile Apps, die sich bei Backend-APIs authentifizieren müssen
- Microservices: Jeder Dienst kann Tokens unabhängig verifizieren, ohne einen zentralen Sitzungsspeicher
- Performance: Eliminiert Datenbankabfragen für jede authentifizierte Anfrage
Sie können jedes JWT mit unserem JWT-Decoder-Tool inspizieren und dekodieren, um seinen Inhalt zu sehen, ohne Code schreiben zu müssen. Dies ist besonders nützlich beim Debuggen von Authentifizierungsproblemen oder beim Verstehen, welche Daten Ihre Tokens enthalten.
Profi-Tipp: JWTs sind standardmäßig signiert, nicht verschlüsselt. Jeder kann ein JWT dekodieren und lesen. Speichern Sie niemals sensible Informationen wie Passwörter, Kreditkartennummern oder Sozialversicherungsnummern in JWT-Payloads.
JWT-Struktur erklärt
Ein JWT besteht aus drei verschiedenen Teilen, die durch Punkte (.) getrennt sind: dem Header, dem Payload und der Signatur. So sieht ein echtes JWT aus:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZW1haWwiOiJqb2huQGV4YW1wbGUuY29tIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDI2MjJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Lassen Sie uns jede Komponente aufschlüsseln, um zu verstehen, wie sie zusammenarbeiten, um ein sicheres, verifizierbares Token zu erstellen.
1. Header
Der Header enthält typischerweise zwei kritische Felder, die definieren, wie das Token verarbeitet werden soll:
{
"alg": "HS256",
"typ": "JWT"
}
alg(Algorithmus): Gibt den kryptografischen Algorithmus an, der zum Signieren des Tokens verwendet wirdtyp(Typ): Deklariert, dass dies ein JWT-Token ist
Dieses JSON-Objekt wird dann Base64Url-kodiert, um den ersten Teil des JWT zu bilden. Die Kodierung macht es URL-sicher und kompakt für die Übertragung in HTTP-Headern.
2. Payload
Der Payload enthält Claims – Aussagen über den Benutzer und zusätzliche Metadaten. Hier speichern Sie die tatsächlichen Daten, die Sie übertragen möchten:
{
"sub": "1234567890",
"name": "John Doe",
"email": "[email protected]",
"role": "admin",
"iat": 1516239022,
"exp": 1516242622
}
Claims werden in drei Kategorien unterteilt:
Registrierte Claims (standardisiert und empfohlen):
sub(Subjekt): Eindeutige Kennung für den Benutzeriat(ausgestellt am): Zeitstempel, wann das Token erstellt wurdeexp(Ablauf): Zeitstempel, wann das Token abläuftiss(Aussteller): Identifiziert, wer das Token ausgestellt hataud(Zielgruppe): Identifiziert, für wen das Token bestimmt istnbf(nicht vor): Token ist vor diesem Zeitstempel nicht gültigjti(JWT-ID): Eindeutige Kennung für das Token selbst
Öffentliche Claims sind benutzerdefinierte Claims, die in der IANA JSON Web Token Registry definiert werden sollten oder kollisionsresistente Namen (wie URLs) verwenden.
Private Claims sind benutzerdefinierte Claims, auf die sich die Parteien geeinigt haben, wie role, permissions oder department.
3. Signatur
Die Signatur macht JWTs sicher und manipulationssicher. Sie wird erstellt, indem der kodierte Header, der kodierte Payload, ein geheimer Schlüssel genommen und der im Header angegebene Algorithmus angewendet wird:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
Die Signatur erfüllt zwei kritische Zwecke:
- Integritätsprüfung: Stellt sicher, dass das Token seit seiner Ausstellung nicht verändert wurde
- Authentifizierung: Beweist, dass das Token von jemandem mit Zugriff auf den geheimen Schlüssel erstellt wurde
Wenn jemand versucht, den Header oder Payload zu ändern, schlägt die Signaturprüfung fehl und das Token wird abgelehnt.
Wie JWT-Authentifizierung funktioniert
Das Verstehen des vollständigen Authentifizierungsablaufs ist für die korrekte Implementierung von JWTs unerlässlich. Hier ist der schrittweise Prozess:
Schritt 1: Benutzeranmeldung
Der Benutzer übermittelt seine Anmeldedaten (Benutzername und Passwort) an den Authentifizierungsendpunkt. Der Server validiert diese Anmeldedaten gegen die Datenbank.
Schritt 2: Token-Generierung
Wenn die Anmeldedaten gültig sind, generiert der Server ein JWT mit Benutzerinformationen und Claims. Der Server signiert das Token mit einem geheimen Schlüssel (für symmetrische Algorithmen) oder einem privaten Schlüssel (für asymmetrische Algorithmen).
Schritt 3: Token-Übermittlung
Der Server sendet das JWT zurück an den Client, typischerweise im Response-Body. Der Client speichert dieses Token für zukünftige Anfragen.
Schritt 4: Authentifizierte Anfragen
Für nachfolgende Anfragen fügt der Client das JWT im Authorization-Header mit dem Bearer-Schema ein:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Schritt 5: Token-Verifizierung
Der Server empfängt die Anfrage, extrahiert das JWT aus dem Header und verifiziert die Signatur mit dem geheimen oder öffentlichen Schlüssel. Wenn gültig, extrahiert der Server Benutzerinformationen aus dem Payload und verarbeitet die Anfrage.
Schritt 6: Token-Ablauf
Wenn das Token abläuft, muss der Client ein neues Token erhalten, entweder durch erneute Authentifizierung oder durch Verwendung eines Refresh-Tokens (im nächsten Abschnitt behandelt).
Schneller Tipp: Validieren Sie immer den exp-Claim auf der Serverseite, auch wenn Sie den Ablauf auf dem Client prüfen. Clientseitige Prüfungen können umgangen werden, aber serverseitige Validierung ist maßgeblich.
Access Tokens vs. Refresh Tokens
Ein robustes JWT-Authentifizierungssystem verwendet typischerweise zwei Arten von Tokens, die zusammenarbeiten: Access Tokens und Refresh Tokens. Das Verständnis des Unterschieds ist entscheidend für den Aufbau sicherer Anwendungen.
Access Tokens
Access Tokens sind kurzlebige JWTs (typischerweise 15 Minuten bis 1 Stunde), die Zugriff auf geschützte Ressourcen gewähren. Sie enthalten Benutzeridentität und Berechtigungen, die zur Autorisierung von Anfragen benötigt werden.
Eigenschaften:
- Kurze Ablaufzeit (5-60 Minuten)
- In jeder API-Anfrage enthalten
- Enthält Benutzerberechtigungen und Rollen
- Kann nicht vor Ablauf widerrufen werden (zustandslos)
Refresh Tokens
Refresh Tokens sind langlebige Tokens (Tage bis Monate), die ausschließlich zum Erhalt neuer Access Tokens verwendet werden. Sie werden sicher gespeichert und nur an den Token-Refresh-Endpunkt gesendet.
Eigenschaften:
- Lange Ablaufzeit (Tage bis Monate)
- Nur an den Refresh-Endpunkt gesendet
- Kann in der Datenbank widerrufen werden
- Oft mit zusätzlichen Metadaten gespeichert (Gerät, IP, User Agent)
Warum beide verwenden?
Dieser Dual-Token-Ansatz balanciert Sicherheit und Benutzererfahrung:
| Aspekt | Nur Access Token | Access + Refresh Tokens |
|---|---|---|
| Sicherheit | Niedriger (langlebige Tokens gefährdet) | Höher (kurzlebige Access Tokens) |
| Benutzererfahrung | Besser (keine erneute Authentifizierung) | Besser (nahtlose Token-Aktualisierung) |
| Widerruf | Schwierig (zustandslos) | Möglich (Refresh Tokens in DB) |
| Netzwerk-Overhead | Niedriger | Etwas höher (Refresh-Anfragen) |
| Implementierungskomplexität | Einfach | Moderat |
Token-Refresh-Ablauf
- Client erkennt, dass Access Token abgelaufen ist oder bald abläuft
- Client sendet Refresh Token an
/auth/refresh-Endpunkt - Server validiert Refresh Token gegen Datenbank
- Server generiert neues Access Token (und optional neues Refresh Token)
- Client empfängt und speichert neue Tokens
- Client wiederholt ursprüngliche Anfrage mit neuem Access Token
Profi-Tipp: Implementieren Sie Refresh-Token-Rotation – geben Sie jedes Mal ein neues Refresh Token aus, wenn eines verwendet wird, und invalidieren Sie das alte. Dies begrenzt den Schaden, wenn ein Refresh Token kompromittiert wird.
Node.js-Implementierungsleitfaden
Lassen Sie uns ein vollständiges JWT-Authentifizierungssystem mit Node.js, Express und der jsonwebtoken-Bibliothek erstellen. Diese Implementierung umfasst sowohl Access- als auch Refresh-Tokens.
Installation
npm install express jsonwebtoken bcrypt dotenv
Umgebungskonfiguration
Erstellen Sie eine .env-Datei mit Ihren geheimen Schlüsseln:
ACCESS_TOKEN_SECRET=your-super-secret-access-key-change-this
REFRESH_TOKEN_SECRET=your-super-secret-refresh-key-change-this
ACCESS_TOKEN_EXPIRY=15m
REFRESH_TOKEN_EXPIRY=7d
Token-Generierung
const jwt = require('jsonwebtoken');
function generateAccessToken(user) {
return jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role
},
process.env.ACCESS_TOKEN_SECRET,
{ expiresIn: process.env.ACCESS_TOKEN_EXPIRY }
);
}
function generateRefreshToken(user) {
return jwt.sign(
{ userId: user.id },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: process.env.REFRESH_TOKEN_EXPIRY }
);
}
Login-Endpunkt
const bcrypt = require('bcrypt');
app.post('/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
// Benutzer in Datenbank finden
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Ungültige Anmeldedaten' });
}
// Passwort verifizieren
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
return res.status(401).json({ error: 'Ungültige Anmeldedaten' });
}
// Tokens generieren
const accessToken = generateAccessToken(user);
const refreshToken = generateRefreshToken(user);
// Refresh Token in Datenbank speichern
await RefreshToken.create({
token: refreshToken,
userId: user.id,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
});
res.json({
accessToken,
refreshToken,
user: {
id: user.id,
email: user.email,
name: user.name
}
});
} catch (error) {
res.status(500).json({ error: 'Serverfehler' });
}
});
Authentifizierungs-Middleware
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access Token erforderlich' });
}
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Ungültiges oder abgelaufenes Token' });
}
req.user = user;
next();
});
}
// Verwendung
app.get('/api/protected', authenticateToken, (req, res) => {
res.json({
message: 'Geschützte Daten',
user: req.user
});
});
Token-Refresh-Endpunkt
app.post('/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh Token erforderlich' });
}
try {
// Prüfen, ob Refresh Token in Datenbank existiert
const storedToken = await RefreshToken.findOne({ token: refreshToken });
if (!storedToken) {
return res.status(403).json({ error: 'Ungültiges Refresh Token' });
}
// Token-Signatur verifizieren
jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, async (err, user) => {
if (err) {
return res.status(403).json({ error: 'Ungültiges Refresh Token' });
}
// Benutzerdaten abrufen
const userData = await User.findById(user.userId);
// Neues Access Token generieren
const accessToken = generateAccessToken(userData);
res.json({ accessToken });