JWT-Tokens erklärt: Struktur, Sicherheit und Best Practices

· 12 Min. Lesezeit

JSON Web Tokens (JWTs) sind zum De-facto-Standard für zustandslose Authentifizierung in modernen Webanwendungen geworden. Egal, ob Sie eine REST-API, eine Microservices-Architektur oder eine Single-Page-Anwendung erstellen – das Verständnis der Funktionsweise von JWTs ist entscheidend für die Implementierung sicherer, skalierbarer Authentifizierung.

Dieser umfassende Leitfaden erklärt alles, was Sie über JWTs wissen müssen – von ihrer internen Struktur bis hin zu produktionsreifen Sicherheitspraktiken. Am Ende werden Sie nicht nur verstehen, wie man JWTs verwendet, sondern auch wann man sie einsetzt und wie man häufige Fallstricke vermeidet, die zu Sicherheitslücken führen.

Inhaltsverzeichnis

Was ist ein JWT?

Ein JSON Web Token (JWT) ist ein kompaktes, URL-sicheres Mittel zur Darstellung von Claims, die zwischen zwei Parteien übertragen werden sollen. Die Claims in einem JWT sind als JSON-Objekt kodiert, das mit JSON Web Signature (JWS) digital signiert wird.

Stellen Sie sich ein JWT als manipulationssicheren Container für Benutzerinformationen vor. Wenn sich ein Benutzer anmeldet, erstellt Ihr Server ein JWT, das dessen Benutzer-ID, Rollen und andere relevante Daten enthält. Dieses Token wird dann an den Client gesendet, der es bei jeder nachfolgenden Anfrage mitsendet. Der Server kann die Authentizität des Tokens überprüfen, ohne eine Datenbank abzufragen, was JWTs ideal für zustandslose, skalierbare Architekturen macht.

JWTs sind durch RFC 7519 definiert und werden in Programmiersprachen und Frameworks weitgehend unterstützt. Sie sind besonders beliebt bei:

Schneller Tipp: Verwenden Sie unseren JWT-Decoder, um jedes JWT-Token sofort zu inspizieren und zu debuggen. Es ist vollständig clientseitig, sodass Ihre Tokens niemals Ihren Browser verlassen.

JWT-Struktur: Header, Payload und Signatur

Ein JWT besteht aus drei Base64URL-kodierten Teilen, die durch Punkte (.) getrennt sind. So sieht ein echtes JWT aus:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Aufgeschlüsselt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9           ← Header
.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ  ← Payload
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c    ← Signatur
Teil Enthält Beispiel (dekodiert)
Header Algorithmus + Token-Typ {"alg": "HS256", "typ": "JWT"}
Payload Claims (Benutzerdaten) {"sub": "1234567890", "name": "John Doe", "iat": 1516239022}
Signatur Verifizierungs-Hash HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

Der Header

Der Header besteht typischerweise aus zwei Teilen: dem Token-Typ (typ) und dem Signaturalgorithmus (alg). Gängige Algorithmen sind:

Der Payload

Der Payload enthält die Claims – Aussagen über eine Entität (typischerweise den Benutzer) und zusätzliche Metadaten. Claims sind nicht verschlüsselt, sondern nur kodiert, daher sollten Sie niemals sensible Informationen wie Passwörter in einem JWT-Payload ablegen.

Die Signatur

Die Signatur stellt sicher, dass das Token nicht manipuliert wurde. Sie wird erstellt, indem der kodierte Header, der kodierte Payload, ein geheimer Schlüssel genommen und der im Header angegebene Algorithmus angewendet wird. Wenn jemand den Header oder Payload ändert, schlägt die Signaturüberprüfung fehl.

Profi-Tipp: Generieren Sie Test-JWTs mit benutzerdefinierten Claims mit unserem JWT-Generator-Tool. Perfekt für Entwicklungs- und Testszenarien.

Standard-Claims (RFC 7519)

RFC 7519 definiert mehrere Standard-Claims, die Interoperabilität zwischen verschiedenen JWT-Implementierungen ermöglichen. Obwohl diese Claims optional sind, verbessert ihre korrekte Verwendung Sicherheit und Kompatibilität.

Claim Name Zweck Beispiel
iss Aussteller Identifiziert, wer das Token erstellt hat "https://auth.example.com"
sub Subjekt Identifiziert, worum es bei dem Token geht (normalerweise Benutzer-ID) "user_12345"
aud Zielgruppe Identifiziert, für wen das Token bestimmt ist "https://api.example.com"
exp Ablauf Wann das Token abläuft (Unix-Zeitstempel) 1735689600
nbf Nicht vor Token ist vor dieser Zeit nicht gültig 1735603200
iat Ausgestellt am Wann das Token erstellt wurde 1735603200
jti JWT-ID Eindeutige Kennung für das Token (nützlich für Widerruf) "a1b2c3d4-e5f6"

Benutzerdefinierte Claims

Über Standard-Claims hinaus können Sie benutzerdefinierte Claims hinzufügen, die spezifisch für Ihre Anwendung sind. Häufige Beispiele sind:

Halten Sie Ihren Payload klein – jedes Byte erhöht den Netzwerk-Overhead. Fügen Sie nur die Claims ein, die Sie für Autorisierungsentscheidungen benötigen. Wenn Sie zusätzliche Benutzerdaten benötigen, rufen Sie diese aus Ihrer Datenbank ab, indem Sie den sub-Claim als Suchschlüssel verwenden.

Wie JWT-Authentifizierung funktioniert

Das Verständnis des vollständigen Authentifizierungsablaufs hilft Ihnen, JWTs korrekt und sicher zu implementieren. Hier ist die typische Abfolge:

  1. Benutzeranmeldung: Benutzer sendet Anmeldedaten (Benutzername + Passwort) an Ihren Authentifizierungs-Endpunkt
  2. Überprüfung der Anmeldedaten: Server validiert Anmeldedaten gegen Ihre Benutzerdatenbank
  3. Token-Generierung: Server erstellt ein JWT mit Benutzer-Claims und signiert es mit einem geheimen Schlüssel
  4. Token-Übermittlung: Server gibt das JWT an den Client zurück (typischerweise im Antworttext oder als httpOnly-Cookie)
  5. Token-Speicherung: Client speichert das JWT sicher (mehr dazu im Abschnitt Speicheroptionen)
  6. Authentifizierte Anfragen: Client fügt das JWT im Authorization-Header für nachfolgende Anfragen ein
  7. Token-Überprüfung: Server überprüft die Signatur und prüft den Ablauf – keine Datenbankabfrage erforderlich
  8. Zugriff gewährt: Wenn gültig, verarbeitet der Server die Anfrage unter Verwendung der Claims aus dem Token

Hier ist eine visuelle Darstellung:

┌────────┐                                  ┌────────┐
│ Client │                                  │ Server │
└───┬────┘                                  └───┬────┘
    │                                           │
    │  POST /login {username, password}         │
    │──────────────────────────────────────────>│
    │                                           │
    │                                    [Anmeldedaten überprüfen]
    │                                    [JWT generieren]
    │                                           │
    │  200 OK {token: "eyJhbG..."}              │
    │<──────────────────────────────────────────│
    │                                           │
[Token speichern]                               │
    │                                           │
    │  GET /api/profile                         │
    │  Authorization: Bearer eyJhbG...          │
    │──────────────────────────────────────────>│
    │                                           │
    │                                    [Signatur überprüfen]
    │                                    [Ablauf prüfen]
    │                                    [Claims extrahieren]
    │                                           │
    │  200 OK {user data}                       │
    │<──────────────────────────────────────────│
    │                                           │

Profi-Tipp: Die Schönheit von JWTs liegt darin, dass Schritt 7 (Token-Überprüfung) keine Datenbankabfrage erfordert. Der Server kann Authentizität überprüfen und Benutzerinformationen direkt aus dem Token extrahieren, was Ihre API hochgradig skalierbar macht.

Implementierungsbeispiele

Schauen wir uns praktische Implementierungen in beliebten Programmiersprachen und Frameworks an.

Node.js mit jsonwebtoken

const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

// Login-Endpunkt
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  // Benutzer in Datenbank finden
  const user = await User.findOne({ username });
  if (!user) {
    return res.status(401).json({ error: 'Ungültige Anmeldedaten' });
  }
  
  // Passwort überprüfen
  const validPassword = await bcrypt.compare(password, user.passwordHash);
  if (!validPassword) {
    return res.status(401).json({ error: 'Ungültige Anmeldedaten' });
  }
  
  // JWT erstellen
  const token = jwt.sign(
    { 
      sub: user.id,
      email: user.email,
      role: user.role 
    },
    process.env.JWT_SECRET,
    { 
      expiresIn: '15m',
      issuer: 'https://api.example.com',
      audience: 'https://example.com'
    }
  );
  
  res.json({ token });
});

// Geschützte Route Middleware
const authenticateToken = (req, res, next) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
  
  if (!token) {
    return res.status(401).json({ error: 'Kein Token bereitgestellt' });
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(403).json({ error: 'Ungültiges oder abgelaufenes Token' });
  }
};

// Middleware verwenden
app.get('/api/profile', authenticateToken, (req, res) => {
  res.json({ userId: req.user.sub, email: req.user.email });
});

Python mit PyJWT

import jwt
import datetime
from flask import Flask, request, jsonify

app = Flask(__name__)
SECRET_KEY = 'your-secret-key'

@app.route('/login', methods=['POST'])
def login():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password')
    
    # Anmeldedaten überprüfen (vereinfacht)
    user = verify_credentials(username, password)
    if not user:
        return jsonify({'error': 'Ungültige Anmeldedaten'}), 401
    
    # JWT erstellen
    payload = {
        'sub': user['id'],
        'email': user['email'],
        'role': user['role'],
        'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=15),
        'iat': datetime.datetime.utcnow()
    }
    
    token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
    return jsonify({'token': token})

def token_required(f):
    def decorator(*args, **kwargs):
        token = request.headers.get('Authorization')
        
        if not token:
            return jsonify({'error': 'Kein Token bereitgestellt'}), 401
        
        try:
            # 'Bearer '-Präfix entfernen
            token = token.split(' ')[1]
            decoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
            request.user = decoded
        except jwt.ExpiredSignatureError:
            return jsonify({'error': 'Token abgelaufen'}), 403
        except jwt.InvalidTokenError:
            return jsonify({'error': 'Ungültiges Token'}), 403
        
        return f(*args, **kwargs)
    
    return decorator

@app.route('/api/profile')
@token_required
def profile():
    return jsonify({'userId': request.user['sub']})

Go mit golang-jwt

package main

import (
    "time"
    "github.com/golang-jwt/jwt/v5"
)

type Claims struct {
    UserID string `json:"sub"`
    Email  string `json:"email"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

func generateToken(userID, email, role string) (string, error) {
    claims := Claims{
        UserID: userID,
        Email:  emai