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.
Inhaltsverzeichnis
- 1. Die grundlegende Frage: Stateless oder Stateful?
- 2. JWT: Aufbau, Signierung und Validierung
- 3. Session-Authentifizierung: Wie Symfony Sessions verwaltet
- 4. Implementierung in Symfony: JWT vs. Session im Code-Vergleich
- 5. Sicherheitsrisiken: XSS, CSRF, Token-Diebstahl und Revokation
- 6. Skalierung und Infrastruktur: Wann JWT wirklich hilft
- 7. Refresh-Tokens und Token-Rotation in Symfony
- 8. JWT vs. Session: Direkter Vergleich
- 9. Zusammenfassung: Wann welche Methode?
- 10. FAQ
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.