{ }
GET
Symfony · JWT · Session · REST-API-Security · Authentifizierung
JWT und session-basierte REST-APIs
in Symfony vergleichen

JWT oder Sessions für Symfony REST-APIs? Die Antwort hängt von Skalierungsanforderungen, Revokationsbedarf, Client-Typ und Sicherheitsmodell ab. Wer den Unterschied zwischen stateless und stateful Authentifizierung versteht, trifft die richtige Entscheidung für seinen konkreten Anwendungsfall.

22 Min. Lesezeit JWT · Sessions · Stateless · Token-Revokation · XSS vs. CSRF Symfony 7 · PHP 8.4 · LexikJWT

1. Die grundlegende Frage: Stateless oder Stateful?

Der Kern der Debatte zwischen JWT und Sessions ist die Frage nach Stateless vs. Stateful Authentifizierung. Bei session-basierter Authentifizierung speichert der Server den Authentifizierungsstatus in einem Session-Store (Datei, Redis, Datenbank). Der Client bekommt eine Session-ID als Cookie, der Server schlägt bei jedem Request den aktuellen Zustand nach. Das bedeutet: Der Server kontrolliert den Zustand vollständig. Eine Session kann jederzeit invalidiert werden, ohne dass der Client informiert werden muss. Der Preis ist ein Server-seitiger Datenbankzugriff (oder Redis-Lookup) bei jedem Authenticated Request.

Bei JWT-basierter Authentifizierung ist der Token selbst das Authentifizierungsdokument. Er enthält alle notwendigen Informationen (User-ID, Rollen, Ablaufzeit) und ist kryptographisch signiert. Der Server muss bei der Validierung keinen externen Store befragen – er prüft nur die Signatur und die Claims. Das macht JWT-Authentifizierung stateless: Jede API-Instanz kann einen Token unabhängig validieren, ohne Koordination mit anderen Instanzen. Der Preis ist, dass ein Token nicht ohne weiteres revokiert werden kann, bevor er abläuft – das ist das fundamentale Problem aller stateless Token-Systeme. Die Entscheidung zwischen JWT und Sessions ist daher primär eine Entscheidung zwischen Skalierbarkeit und Kontrollierbarkeit.

2. JWT: Aufbau, Signierung und Validierung

Ein JSON Web Token (JWT) besteht aus drei Base64-URL-kodierten Teilen, getrennt durch Punkte: Header, Payload und Signatur. Der Header enthält den Token-Typ (JWT) und den Signaturalgorithmus (RS256 für RSA mit SHA-256 oder HS256 für HMAC mit SHA-256). Der Payload enthält standardisierte Claims (sub für Subject/User-ID, exp für Ablaufzeit, iat für Ausstellungszeitpunkt, iss für Issuer) und benutzerdefinierte Claims (z.B. Rollen, Tenant-ID). Die Signatur wird aus Header und Payload mit dem geheimen Schlüssel erzeugt und verhindert Manipulation.

Für produktive Symfony-APIs empfiehlt sich RS256 (asymmetrisch) statt HS256 (symmetrisch). Bei RS256 hält der Authentifizierungsserver den privaten Schlüssel zum Signieren, alle API-Instanzen kennen nur den öffentlichen Schlüssel zur Validierung. Das ermöglicht eine echte Trennung zwischen Token-Ausstellung und Token-Validierung – und verhindert, dass eine kompromittierte API-Instanz neue Tokens ausstellen kann. HS256 mit einem shared Secret ist einfacher aufzusetzen, aber riskant in verteilten Systemen: Jede Instanz, die validieren kann, kann auch ausstellen. Die Ablaufzeit (exp) sollte kurz gewählt werden – 15 Minuten bis 1 Stunde für Access-Tokens ist ein guter Ausgangspunkt.

3. Session-Authentifizierung: Wie Symfony Sessions verwaltet

Symfony's Session-System basiert auf PHP-Sessions mit konfigurierbarem Handler. Standardmäßig werden Sessions als Dateien gespeichert, in produktiven Umgebungen empfiehlt sich ein Redis- oder Datenbank-Handler. Der Session-Mechanismus für REST-APIs funktioniert mit einem Session-Cookie, den der Browser automatisch bei jedem Request mitsendet. Das bedeutet: Session-Authentifizierung ist primär für Browser-basierte Clients geeignet, nicht für mobile Apps oder serverseitige API-Clients, die Cookie-Management selbst implementieren müssten.

Der Vorteil von Sessions ist die vollständige serverseitige Kontrolle: Eine Benutzer-Session kann sofort invalidiert werden – durch Abmeldung, nach einer verdächtigen Aktion, oder durch einen Administrator. Alle aktiven Sessions eines Benutzers sind auflistbar und verwaltbar. In Symfony geht das mit $tokenStorage->setToken(null) oder durch direktes Löschen des Session-Eintrags im Session-Handler. Bei JWT ist Invalidierung ohne Blocklist-Mechanismus erst nach dem natürlichen Ablauf des Tokens möglich. Für Anwendungen, bei denen schnelle Revokation eine Sicherheitsanforderung ist (Finanzanwendungen, Behördenanwendungen), ist das ein entscheidender Nachteil von JWT ohne zusätzliche Komplexität.

<?php
// Symfony 7: JWT-Authentifizierung mit LexikJWTAuthenticationBundle
// security.yaml (Ausschnitt)

// JWT-Konfiguration
// config/packages/lexik_jwt_authentication.yaml:
// lexik_jwt_authentication:
//   secret_key: '%env(resolve:JWT_SECRET_KEY)%'
//   public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
//   pass_phrase: '%env(JWT_PASSPHRASE)%'
//   token_ttl: 3600  # 1 Stunde

// Login-Controller: JWT ausstellen
declare(strict_types=1);

namespace App\Controller\Api;

use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use App\Repository\UserRepository;

/**
 * Issues JWT tokens on successful login.
 */
#[Route('/api/auth/login', name: 'api_auth_login', methods: ['POST'])]
final class LoginController extends AbstractController
{
    public function __construct(
        private readonly UserRepository $userRepository,
        private readonly UserPasswordHasherInterface $hasher,
        private readonly JWTTokenManagerInterface $jwtManager,
    ) {}

    public function __invoke(Request $request): JsonResponse
    {
        $data     = json_decode($request->getContent(), true);
        $email    = $data['email'] ?? '';
        $password = $data['password'] ?? '';

        $user = $this->userRepository->findOneByEmail($email);

        if ($user === null || !$this->hasher->isPasswordValid($user, $password)) {
            throw new BadCredentialsException('Ungültige Anmeldedaten.');
        }

        $token = $this->jwtManager->create($user);

        return $this->json([
            'token'     => $token,
            'expiresIn' => 3600,
            'tokenType' => 'Bearer',
        ]);
    }
}

4. Implementierung in Symfony: JWT vs. Session im Code-Vergleich

In Symfony konfiguriert man JWT-Authentifizierung am einfachsten mit dem LexikJWTAuthenticationBundle. Das Bundle übernimmt Token-Ausstellung, Validierung und Integration in das Symfony Security-System. Der Firewall in security.yaml wird auf stateless: true gesetzt – das bedeutet, Symfony startet keine Session für API-Requests. Jeder Request wird unabhängig durch den JWT-Authenticator validiert. Das Ergebnis: Kein Session-Overhead, keine Session-Cookie-Anforderungen, und die API kann horizontal skaliert werden, ohne Session-Sharing zu konfigurieren.

Session-basierte Authentifizierung in Symfony für REST-APIs nutzt denselben Security-Stack, aber mit stateless: false und einem Cookie-basierten Authenticator. Für mobile und API-Clients, die kein Cookie-Management haben, braucht man zusätzlich einen X-AUTH-TOKEN-Header-Mechanismus oder man akzeptiert, dass Sessions für diese Clients nicht geeignet sind. Der praktische Unterschied im Code: JWT-APIs geben bei POST /login einen Token-String zurück, den der Client im Authorization-Header sendet. Session-APIs setzen ein Cookie, das der Browser automatisch mitschickt. Für SPAs (Single Page Applications) ist JWT mit localStorage oder sessionStorage üblich – mit dem Risiko, XSS-anfällig zu sein. Für traditionelle Browser-Apps ist das Session-Cookie mit httpOnly: true sicherer.

5. Sicherheitsrisiken: XSS, CSRF, Token-Diebstahl und Revokation

Der wichtigste Sicherheitsunterschied zwischen JWT und Sessions liegt im Angriffsvektor. Session-Cookies mit httpOnly: true sind vor JavaScript-Zugriff geschützt – ein XSS-Angriff kann das Cookie nicht lesen. Der Angriff auf Sessions ist CSRF (Cross-Site Request Forgery): Ein angreifender Tab kann einen Request mit dem Cookie des Opfers auslösen. Symfony schützt vor CSRF mit einem synchronisierten Token-Pattern – das ist standardmäßig aktiv und gut erprobt. JWT im Browser-Storage ist dagegen anfällig für XSS: Jedes XSS-Skript kann den Token aus localStorage lesen und ihn verwenden, bis er abläuft. Das ist ein fundamentaler Unterschied: Session-Cookies sind CSRF-anfällig, JWT in Storage ist XSS-anfällig. In beiden Fällen gibt es Gegenmittel, aber keiner der Ansätze ist inhärent sicherer.

Das größte operative Sicherheitsproblem bei JWT: Token-Revokation. Wenn ein JWT-Token gestohlen wird oder ein Benutzer sofort abgemeldet werden soll (z.B. bei Kontoübernahme), kann der Token nicht sofort ungültig gemacht werden – er läuft erst bei exp ab. Die Lösung ist eine Blocklist (Deny-List) in Redis oder der Datenbank, die invalidierte Token-IDs (jti-Claim) bis zu ihrem natürlichen Ablauf speichert. Das führt wieder zu einem serverseitigen Lookup bei jedem Request – womit der Stateless-Vorteil von JWT teilweise aufgegeben wird. Kurze Token-Lebenszeiten (15 Minuten) mit Refresh-Token-Rotation minimieren das Risiko, ohne vollständige Blocklist zu erfordern.

<?php
// JWT-Blocklist für sofortige Token-Revokation in Symfony
declare(strict_types=1);

namespace App\Service;

use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;

/**
 * Token blocklist using Symfony Cache (Redis or APCu).
 * Stores revoked JWT IDs (jti) until their natural expiry.
 */
final readonly class JwtBlocklist
{
    public function __construct(
        private CacheInterface $cache,
    ) {}

    /**
     * Add a JWT ID to the blocklist until its expiry timestamp.
     */
    public function revoke(string $jti, int $expiresAt): void
    {
        $ttl = max(0, $expiresAt - time());

        $this->cache->get('jwt_blocklist_' . $jti, function (ItemInterface $item) use ($ttl): bool {
            $item->expiresAfter($ttl);
            return true; // Value is irrelevant — presence means revoked
        });
    }

    /**
     * Check if a JWT ID is in the blocklist (i.e., revoked).
     */
    public function isRevoked(string $jti): bool
    {
        return $this->cache->hasItem('jwt_blocklist_' . $jti);
    }
}

// Usage in JWT validation event listener:
// if ($blocklist->isRevoked($payload['jti'])) {
//     throw new InvalidTokenException('Token has been revoked.');
// }

6. Skalierung und Infrastruktur: Wann JWT wirklich hilft

Der Skalierungsvorteil von JWT ist real, aber oft überschätzt. JWT-Authentifizierung hilft, wenn die API auf mehreren unabhängigen Instanzen läuft, die keinen gemeinsamen Session-Store haben. Jede Instanz kann einen JWT-Token validieren, ohne mit anderen Instanzen zu kommunizieren – ein RSA-Public-Key, der beim Start geladen wird, reicht für alle Validierungen. Das reduziert Latenz (kein Redis-Roundtrip pro Request) und macht die API horizontal skalierbar, ohne Session-Affinity (Sticky Sessions) zu konfigurieren.

Session-Authentifizierung erfordert einen gemeinsamen Session-Store, wenn mehrere Instanzen laufen. In der Praxis ist das kein unüberwindbares Problem: Redis als Session-Handler ist bewährt und fügt typischerweise weniger als 1ms Latenz hinzu. Für kleine bis mittelgroße Anwendungen mit einem Load-Balancer ist Session-Authentifizierung mit Redis-Session-Store vollständig ausreichend. JWT macht bei Microservices mehr Sinn als bei monolithischen APIs: Wenn Service A einen Token ausstellt und Service B und C ihn validieren müssen, ohne Service A zu kontaktieren, ist JWT die elegante Lösung. Wenn alles in einem Symfony-Monolithen läuft, ist der Skalierungsvorteil von JWT minimal.

7. Refresh-Tokens und Token-Rotation in Symfony

Kurze Access-Token-Lebenszeiten (15–60 Minuten) lösen das Revokationsproblem erheblich: Selbst wenn ein Token gestohlen wird, ist er in kurzer Zeit wertlos. Der Preis: Der Client muss sich alle 15–60 Minuten erneut authentifizieren – was für Benutzer inakzeptabel ist. Die Lösung ist das Refresh-Token-Muster: Ein kurzlebiger Access-Token (15 Minuten) und ein langlebiger Refresh-Token (7–30 Tage) werden bei der ersten Authentifizierung ausgestellt. Der Access-Token wird für API-Requests verwendet. Wenn er abläuft, tauscht der Client den Refresh-Token gegen ein neues Access-Token und einen neuen Refresh-Token aus (Token-Rotation).

In Symfony implementiert man das mit dem gesdinet/jwt-refresh-token-bundle oder einer eigenen Implementierung. Refresh-Tokens werden in der Datenbank gespeichert und können gezielt revokiert werden. Token-Rotation bedeutet: Bei jedem Refresh wird der alte Refresh-Token ungültig und ein neuer ausgestellt. Wird ein Refresh-Token zweimal verwendet (was nur passiert, wenn er gestohlen wurde und der Angreifer zuerst refresht hat), sollten beide Tokens – das gerade ausgestellte und das parallel verwendete – sofort revokiert werden. Das ist das Refresh-Token-Reuse-Detection-Muster und ein wesentliches Sicherheitsfeature jeder produktionsreifen JWT-Implementierung.

8. JWT vs. Session: Direkter Vergleich

Die Entscheidung hängt von konkreten Anforderungen ab – nicht von Hype oder Konvention. Hier die direkten Gegenüberstellungen der relevantesten Eigenschaften:

Eigenschaft JWT (Stateless) Session (Stateful)
Server-State Kein State nötig (ohne Blocklist) Session-Store erforderlich (Redis)
Horizontale Skalierung Einfach – kein Shared Store Shared Redis-Store nötig
Sofortige Revokation Nur mit Blocklist (Redis-Lookup) Sofort, ohne Zusatzaufwand
CSRF-Risiko Keines (kein Cookie benötigt) Ja – CSRF-Schutz nötig
XSS-Risiko für Token Ja – wenn in localStorage Nein – httpOnly Cookie
Microservices Ideal – kein zentraler Auth-Server nötig Komplex – Session-Sharing zwischen Services
Client-Typ Mobile, API-Clients, SPA Browser-Apps mit httpOnly Cookie

Ein wichtiger Punkt, der in vielen Vergleichen fehlt: Für Symfony-Anwendungen, die sowohl eine Web-App als auch eine REST-API bedienen, ist eine hybride Lösung oft die pragmatischste Wahl. Die Web-App nutzt Sessions mit httpOnly-Cookies, die REST-API-Endpunkte für externe Clients nutzen JWT. Symfony unterstützt mehrere Security-Firewalls, die unterschiedliche Authentifizierungsmechanismen für unterschiedliche Pfade konfigurieren können. Das vermeidet den Zwang, sich zwischen den Ansätzen zu entscheiden, wenn verschiedene Client-Typen verschiedene Sicherheitsprofile brauchen.

9. Zusammenfassung: Wann welche Methode?

JWT ist die richtige Wahl für: Mobile Apps und serverseitige API-Clients (kein Cookie-Management), Microservice-Architekturen mit mehreren Services, die denselben Token validieren, horizontal skalierte APIs ohne zentralen Session-Store, und APIs, bei denen kurze Token-Lebenszeiten und Refresh-Token-Rotation das Revokationsproblem ausreichend mitigieren. Das LexikJWTAuthenticationBundle macht die Symfony-Integration unkompliziert. Die kritischen Implementierungsdetails: RS256 statt HS256, kurze Access-Token-Lebenszeiten (15–60 Minuten), Refresh-Token-Rotation, und Reuse-Detection als Sicherheitsnetz.

Session-Authentifizierung ist die richtige Wahl für: Browser-basierte Anwendungen mit httpOnly-Cookie und CSRF-Schutz, Anwendungen mit sofortigem Revokationsbedarf (Finanz- und Sicherheitsanwendungen), Symfony-Monolithen ohne Microservice-Verteilung, und Teams, die das einfachere Setup bevorzugen. Redis als Session-Handler ist produktionsreif und bewährt. Die Latenz eines Redis-Lookups pro Request (unter 1ms) ist für die meisten Anwendungen kein messbarer Nachteil. Der Ausgangspunkt für die Entscheidung: Client-Typ und Revokationsanforderungen sind wichtiger als theoretische Skalierbarkeit, die in vielen Projekten nie relevant wird.

JWT vs. Session in Symfony — Das Wichtigste auf einen Blick

JWT: Wann verwenden

Mobile Apps, Microservices, horizontale Skalierung. RS256-Signatur, kurze Lebenszeit, Refresh-Token-Rotation. LexikJWTAuthenticationBundle für Symfony.

Session: Wann verwenden

Browser-Apps mit httpOnly-Cookie, sofortiger Revokationsbedarf, Symfony-Monolithen. Redis als Session-Handler. CSRF-Schutz standardmäßig aktiv in Symfony.

JWT-Sicherheit

RS256 statt HS256. Token nie in localStorage ohne XSS-Schutz. Blocklist für sofortige Revokation. Reuse-Detection bei Refresh-Token-Rotation.

Hybride Lösung

Symfony unterstützt mehrere Security-Firewalls. Web-App mit Sessions, REST-API mit JWT. Kein Zwang, einen Ansatz für alle Clients zu verwenden.

10. FAQ: JWT und session-basierte REST-APIs in Symfony

1Hauptunterschied JWT vs. Sessions?
JWT stateless: Token enthält alle Informationen, kein Server-Lookup nötig. Sessions stateful: Server speichert Zustand, Client hat nur Session-ID. JWT skaliert besser, Sessions ermöglichen sofortige Revokation.
2Wann JWT in Symfony verwenden?
Mobile Apps, API-Clients ohne Cookie-Support, Microservices mit mehreren validierenden Services, horizontale Skalierung. LexikJWTAuthenticationBundle für einfache Integration.
3Wann Sessions die bessere Wahl?
Browser-Apps mit httpOnly-Cookie, sofortiger Revokationsbedarf, Symfony-Monolithen. Redis als Session-Handler. CSRF-Schutz standardmäßig in Symfony aktiv.
4JWT-Tokens revokieren?
Blocklist in Redis mit jti-Claim bis natürlichem Ablauf. Alternativ: kurze Lebenszeiten (15 Min.) mit Refresh-Token-Rotation minimieren Revokationsproblem ohne vollständige Blocklist.
5Warum RS256 statt HS256?
RS256 asymmetrisch: Auth-Server signiert mit privatem Schlüssel, API-Instanzen validieren mit öffentlichem. Kein API-Server kann neue Tokens ausstellen. HS256: jeder validierende Server kann auch ausstellen.
6Was ist das Refresh-Token-Muster?
Kurzlebiger Access-Token (15–60 Min.) + langlebiger Refresh-Token (7–30 Tage). Bei Refresh: alter Token ungültig, neues Paar ausgestellt. Minimiert Revokationsproblem ohne Blocklist.
7Ist JWT sicherer als Sessions?
Weder noch – andere Angriffsvektoren. JWT in localStorage XSS-anfällig. Sessions mit httpOnly CSRF-anfällig (Symfony schützt standardmäßig). Richtige Implementierung bei beiden sicher.
8JWT in Symfony konfigurieren?
LexikJWTAuthenticationBundle installieren. RSA-Schlüsselpaar generieren. Firewall auf stateless: true. Login-Endpunkt implementieren. Bundle übernimmt Validierung automatisch.
9Was ist Refresh-Token-Reuse-Detection?
Refresh-Token zweimal verwendet → gestohlen. Reaktion: alle Session-Token sofort revokieren. gesdinet/jwt-refresh-token-bundle unterstützt dieses Muster für Symfony.
10JWT und Sessions gleichzeitig in Symfony?
Ja. Mehrere Security-Firewalls für unterschiedliche Pfade. Web-App mit Sessions, REST-API mit JWT. Hybride Lösung für verschiedene Client-Typen ohne Kompromisse.