CodeGym /Kurse /Spring Security /Vergleich von formLogin, Basic und stateless

Vergleich von formLogin, Basic und stateless

Spring Security
Level 21, Lektion 3
Verfügbar

1. Vergleich der Modelle: warum und wann

Ehrlich gesagt, der gefährlichste Moment beim Erlernen von Security ist, wenn ein Student ein neues Wort lernt (z. B. stateless) und beschließt, dass dies jetzt die Antwort auf alles ist, einschließlich der Frage „was esse ich zu Abend“. Der Vergleich der Modelle ist nicht dazu da, das „modernste“ auszuwählen, sondern um Preis und Verantwortung jedes einzelnen zu verstehen: wer speichert den Zustand, was muss der Client tun, wie sieht eine wiederholte Anfrage aus und wo verstecken sich am häufigsten unerwartete 401.

Nach dem expliziten JSON‑Login verwechselt man besonders leicht zwei Dinge: den Zeitpunkt des Logins und das Verhalten jeder darauffolgenden Anfrage. Daher ist der Vergleich hier nicht dekorativ: er ordnet ein, was formLogin macht, was Basic macht und was ein stateless‑API erfordert.

Hier ist das besonders wichtig, weil das Domänenmodell bereits aufgebaut ist: Rollen, Authorities, Owner‑Only‑Regeln und @PreAuthorize leben in Ihrem Projekt unabhängig davon, wie der Benutzer genau „beweist“, dass er er selbst ist. Heute halten wir fest, dass sich der Authentifizierungstransport ändert, nicht aber die geschäftliche Autorisierung.

Vier Vergleichsfragen

Damit der Vergleich nicht zu einer Diskussion „mir gefällt Grün besser“ wird, ist es sinnvoll, sich auf Koordinaten zu einigen. Wir vergleichen anhand von vier Fragen: wo lebt der Zustand, wie sieht der erste Login aus, was sendet der Client bei einer wiederholten Anfrage, und wer ist dieser Client typischerweise. Wenn man diese vier Fragen im Kopf behält, verschwindet ein Großteil der Verwirrung von selbst – wie ein Bug nach dem Hinzufügen eines println (nicht immer, aber manchmal angenehm).

Hier ist eine kompakte Tabelle, die wir in den folgenden Abschnitten mit Fleisch bekleiden werden:

Modell Wo lebt der auth‑state zwischen den Anfragen? Wie sieht der erste Login aus? Was bei der wiederholten Anfrage? Typischer Client
formLogin (session‑basiert) Auf dem Server (in der Session), der Client hat nur die Kennung (Cookie) Browser‑Login‑Formular (/login) Der Browser sendet automatisch das Cookie Browser
HTTP Basic Nirgends (im Idealfall), jede Anfrage ist „eigenständig“ Kein „Login als Ereignis“: einfach die erste Anfrage mit Credentials Jede Anfrage trägt Credentials Postman/curl/Skript/Integration
Stateless JSON Login Nirgends auf dem Server, der Zustand liegt „in den Händen des Clients“ Expliziter Endpunkt POST /api/auth/login mit JSON Der Client legt bei jeder Anfrage einen „Zugriffsnachweis“ bei SPA/Mobile/externe Clients

Gleich ein wichtiger Hinweis (der Stunden Leben rettet): In allen drei Modellen taucht innerhalb der Anfrage trotzdem der SecurityContext auf. Der Unterschied liegt nicht darin, ob es einen SecurityContext „gibt oder nicht“, sondern wie er bei jeder Anfrage entsteht und wodurch.

3. formLogin: Session und „Login als Ereignis“

formLogin wird oft als „veraltet“ kritisiert, aber das ist ungefähr so, als würde man eine Gabel kritisieren, weil es Essen zum Liefern gibt. Für den Browser ist dies das natürliche Modell: Der Benutzer gibt einmal das Passwort ein, der Browser transportiert das Cookie von selbst, und der Server merkt sich das Authentifizierungsergebnis. Wichtig zu verstehen: formLogin ist nicht nur eine „Login‑Seite“, sondern ein Verbund: Login‑Formular → erfolgreiche Authentifizierung → Speicherung des Security‑Kontexts zwischen Anfragen → verständlicher Logout.

Anfrageablauf: allgemeines Schema

Wir zeichnen eine „filmische“ Sequenz. Das ist kein Code, sondern ein mentales Modell:

sequenceDiagram
    participant B as Browser
    participant S as Secure Content API

    B->>S: "GET /api/me (ohne Login)"
    S-->>B: "302 Redirect -> /login (oder 401, falls Sie API‑only semantics verwenden)"
    B->>S: "GET /login (Formular)"
    S-->>B: "200 HTML Login‑Seite"
    B->>S: "POST /login (username+password)"
    S-->>B: "Set-Cookie: JSESSIONID=..."
    B->>S: "GET /api/me (Cookie automatisch angehängt)"
    S-->>B: "200 OK (Benutzerinfo)"

Wenn Sie in Ihrem Projekt REST‑freundliches Verhalten implementiert haben (JSON 401 statt Redirect), ändert sich die Logik leicht: Der Browser erhält keinen „automatischen UI‑Pfad“ über /login als Redirect, und Sie entscheiden selbst, ob überhaupt ein HTML‑Login‑Flow benötigt wird. Aber das Fundament von formLogin bleibt: Der Login erfolgt einmal, danach „merkt“ es sich der Server.

Minimaler SecurityFilterChain für formLogin

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Bean
SecurityFilterChain formLoginChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(auth -> auth
            // Öffentliche APIs explizit freigeben, der Rest nur für authentifizierte Benutzer
            .requestMatchers("/api/public/**").permitAll()
            .anyRequest().authenticated()
    );
    // Standard‑formLogin‑Flow aktivieren (Login‑Seite, Handler für POST /login usw.)
    http.formLogin(Customizer.withDefaults());
    return http.build();
}

Hier gibt es fast keine Magie: wir sagen „alles außer public ist gesperrt“ und aktivieren formLogin. Der Rest erledigt Spring Security automatisch, indem es die benötigten Filter hinzufügt, die Sie durch den Login‑Flow führen.

SecurityContext: wie funktioniert das „Gedächtnis“

Der Sinn des session‑basierten Modells ist, dass der Server nach einem erfolgreichen Login das Authentifizierungsergebnis (und den zugehörigen SecurityContext) so speichert, dass bei der nächsten Anfrage der „aktuelle Benutzer“ ohne erneute Passwortprüfung wiederhergestellt werden kann. Daher schreiben Sie im Geschäftscode weiterhin gewohnt:

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
class MeController {

    @GetMapping("/api/me")
    String me(@AuthenticationPrincipal UserDetails user) {
        // user stammt aus dem SecurityContext (der aus der Session wiederhergestellt wurde)
        return user.getUsername(); // z. B.: "alice"
    }
}

Der Geschäfts‑Endpunkt „weiß“ nicht, welchen Authentifizierungstransport Sie verwenden. Er weiß nur: wenn der Benutzer authentifiziert ist, kommt UserDetails.

4. HTTP Basic: „Personalausweis für den Wachmann“

HTTP Basic ist ein unglaublich ehrliches Modell. Es verbirgt fast keine Mechanik: jede geschützte Anfrage trägt Credentials, der Server prüft sie jedes Mal, und es gibt kein „wir erinnern uns, dass du gestern da warst“ (wenn Sie nicht separat eine Session aktiviert haben). Dies ist eine ideale Vergleichsoption, weil sie schnell die Illusion heilt, Authentifizierung sei „ein einziger Login‑Knopf“.

Anfrageablauf

sequenceDiagram
    participant C als "Client (curl/Postman)"
    participant S als Secure Content API

    C->>S: "GET /api/me (ohne Credentials)"
    S-->>C: 401 Unauthorized
    C->>S: "GET /api/me (+ Credentials im Header)"
    S-->>C: "200 OK (Benutzerinfo)"
    C->>S: "GET /api/drafts (+ erneut Credentials)"
    S-->>C: "200 OK (Entwürfe)"

In diesem Modell ist „Login“ kein separates Ereignis (wie ein Login‑Formular). „Login“ ist einfach die erste erfolgreiche Anfrage mit Credentials, und jede folgende Anfrage muss diesen Trick wiederholen.

Minimale Konfiguration des SecurityFilterChain für Basic

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Bean
SecurityFilterChain basicChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(auth -> auth
            // Öffentliche Endpunkte – ohne Autorisierung zugänglich
            .requestMatchers("/api/public/**").permitAll()
            // Alles andere – nur mit gültigen Credentials
            .anyRequest().authenticated()
    );
    // Wichtig: Erstellung/Nutzung der HttpSession verbieten, um Modelle nicht zu vermischen
    http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
    // Challenge/Response HTTP Basic aktivieren
    http.httpBasic(Customizer.withDefaults());
    return http.build();
}

Hier habe ich absichtlich STATELESS hinzugefügt, damit Basic ein „reiner“ Per‑Request‑Mechanismus bleibt und nicht zu „Basic + zufällig auftauchende Session“ wird. So lässt es sich leichter vergleichen.

Test mit curl

Die Befehle sind kurz, aber sehr aussagekräftig:

# 1) Ohne Credentials – wir erwarten 401
curl -i http://localhost:8080/api/me
# HTTP/1.1 401

# 2) Mit Credentials – wir erwarten 200
curl -i -u alice:password http://localhost:8080/api/me
# HTTP/1.1 200
# alice

Ja, das ist „zu einfach“, und genau deshalb ist es nützlich im Kopf: Sie sehen buchstäblich, was der Server jedes Mal braucht.

Wichtiger praktischer Sicherheitsaspekt

HTTP Basic erfordert fast immer HTTPS, weil der Client bei jeder Anfrage das Passwort mitsendet (es wird kodiert, aber nicht „im Sicherheitssinn“ verschlüsselt). In einem Übungsprojekt sprechen wir das als technische Tatsache an: Basic ohne HTTPS ist wie das Tragen der Wohnungsschlüssel an einer Schnur mit dem Schild „das sind die Wohnungsschlüssel“.

5. Stateless JSON Login: Login‑Endpunkt und wiederholte Anfragen

Stateless JSON Login wird oft so verstanden: „Na, wir haben /api/auth/login gemacht, und jetzt ist alles stateless“. Das ist die halbe Wahrheit: Ja, der Login wird explizit, API‑freundlich, ohne HTML und Redirects. Aber Stateless ist immer eine Geschichte über wiederholte Anfragen: Wenn der Server keine Erinnerung zwischen Anfragen speichert, muss der Client etwas mitschicken, das es erlaubt, den Benutzer bei jeder Anfrage wiederherzustellen.

Heute ist uns die Pflicht des Clients an sich wichtig: Jede geschützte Anfrage muss authentifizierbar sein. Das genaue Format dieses Nachweises ist vorerst nicht entscheidend.

Anfrageablauf

sequenceDiagram
    participant A als "API Client (SPA/Mobile)"
    participant S als Secure Content API

    A->>S: "POST /api/auth/login (JSON Credentials)"
    S-->>A: "200 OK (Login‑Antwort)"
    A->>S: "GET /api/me (+ Zugriffsnachweis)"
    S-->>A: "200 OK (Benutzerinfo)"
    A->>S: "GET /api/drafts (+ Zugriffsnachweis)"
    S-->>A: "200 OK (Entwürfe)"

Login als separates Ereignis

Hier ist uns nicht die Form des Controllers wichtig, sondern die Mechanik selbst. Der Client sendet JSON‑Credentials an POST /api/auth/login, die Anwendung erstellt eine nicht authentifizierte Authentication und übergibt sie an den AuthenticationManager. Bei Erfolg wird eine Login‑Antwort zurückgegeben. Damit endet der Login‑Moment.

Die folgende GET /api/me lebt jedoch nach anderen Regeln. Im stateless‑Modell beginnt der Server nicht automatisch, sich den Benutzer nach dem 200 OK zu merken; daher muss jede geschützte Anfrage trotzdem einen separaten Zugriffsnachweis mitbringen.

Konfiguration des stateless‑Modus (ohne Formular und ohne Basic)

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

@Bean
SecurityFilterChain statelessChain(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests(auth -> auth
            // Login und öffentliche Endpunkte ohne Authentifizierung erlauben
            .requestMatchers("/api/public/**", "/api/auth/login").permitAll()
            // Alles andere schützen
            .anyRequest().authenticated()
    );
    // Dem Server verbieten, die Authentifizierung zwischen Anfragen zu „merken“
    http.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
    return http.build();
}

Jetzt passiert etwas Wichtiges und anfangs leicht Kränkendes (das ist normal): Sie loggen sich erfolgreich über /api/auth/login ein, aber die nächste Anfrage an /api/me wird nicht automatisch authentifiziert, weil Sie dem Server verboten haben, die „Login‑Erinnerung“ zu speichern. Das ist kein Bug, sondern der Punkt, an dem das Gehirn endgültig aufhört, „Login‑Endpunkt“ mit „Mechanismus der wiederholten Authentifizierung“ zu verwechseln.

Zusammenfassende Tabelle: Gemeinsamkeiten und Unterschiede

Das Nützlichste am Vergleich ist zu sehen, dass sich 80 % der Anwendung überhaupt nicht ändern. Controller, Services, @PreAuthorize, Ownership‑Checks – all das lebt oberhalb des SecurityContext. Unterschiedlich ist, wie der SecurityContext bei einer Anfrage entsteht und was der Client im Rucksack mitbringen muss.

Hier ist eine „kompaktere“ Tabelle (nicht zum Auswendiglernen, sondern zur Orientierung):

Frage formLogin HTTP Basic Stateless JSON Login
Wo wird das Authentifizierungsergebnis zwischen Anfragen gespeichert? In der HttpSession auf dem Server Nirgends (jede Anfrage neu) Beim Client (Server stateless)
Ist „Login“ ein separates Ereignis? Ja (Login‑Formular / Login‑Endpunkt des Formulars) Nein (einfach die erste Anfrage mit Credentials) Ja (expliziter Endpunkt POST /api/auth/login)
Was wird jeder geschützten Anfrage beigelegt? Cookie mit der Session‑ID Credentials Explizit vom Client übermittelter Zugriffsnachweis
Ist ein Logout als separate Operation nötig? Meistens ja (Session‑Invalidierung) Im Grunde nein (einfach aufhören, Credentials zu senden) Meistens „Logout“ beim Client (Nachweis verloren); serverseitige Revokation ist eine separate Geschichte
Für wen ist es am natürlichsten? Für den Browser Für Werkzeuge/Skripte/einfache Integrationen Für SPA/Mobile und externe API‑Clients

Und noch ein Gedanke, den man ruhig aussprechen sollte: Die Autorisierung bleibt dieselbe. Wenn Sie die Regel „In den Editor‑Bereich darf nur EDITOR“ hatten, bleibt diese Regel bestehen. Ändern tut sich nur, „wie der Benutzer bewiesen hat, dass er EDITOR ist“.

6. Anwendung auf die Secure Content Platform API

Wenn man die Modelle in einem Projekt vergleichen möchte, ist es praktisch, dafür einen separaten Sandbox‑Modus einzurichten. Sie aktivieren formLogin, beobachten das Verhalten von /api/me, wechseln zu Basic, dann zu stateless. In der Produktionskonfiguration sollten Sie nicht alle drei Modelle als dauerhafte Konstruktion halten.

Für einen solchen kontrollierten Vergleich eignen sich Spring Profiles gut: Sie haben mehrere isolierte Sicherheitskonfigurationen und aktivieren explizit eine über spring.profiles.active.

Zum Beispiel können Sie drei Konfigurationen erstellen und sie über spring.profiles.active aktivieren.

Beispiel: Profile für drei Modelle

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
@Profile("security-form")
class FormSecurityConfig {
    // Hier deklarieren Sie den @Bean SecurityFilterChain für formLogin
}

@Configuration
@Profile("security-basic")
class BasicSecurityConfig {
    // Hier deklarieren Sie den @Bean SecurityFilterChain für httpBasic + STATELESS
}

@Configuration
@Profile("security-stateless")
class StatelessSecurityConfig {
    // Hier deklarieren Sie den @Bean SecurityFilterChain für JSON Login + STATELESS
}

Dann können Sie die Anwendung so starten:

# Explizit einen Modus wählen, damit sich die Modelle nicht „bekämpfen“
./gradlew bootRun --args='--spring.profiles.active=security-basic'

Der Sinn des Tricks ist einfach: Sie diskutieren nicht mit sich selbst „was ist jetzt aktiviert?“, sondern wählen es explizit aus. Das ist besonders nützlich, wenn Sie das Verhalten von /api/me, /api/drafts, /api/editor/** und /api/admin/** in verschiedenen Auth‑Modellen vergleichen. Letztendlich lebt das Projekt aber immer mit einem ausgewählten Sicherheitsmodell, nicht mit drei gleichzeitig.

7. Typische Fehler beim Vergleich und der Modellauswahl

Fehler Nr. 1: Das Modell nach dem Motto „Stateless ist moderner, also besser“ auswählen.
Eine solche Wahl klingt schwungvoll, endet aber traurig: Sie bekommen eine Architektur, die Ihrem Client nicht passt. Wenn das Produkt ein browserbasiertes Dashboard ist, kann session‑basiert natürlicher und günstiger in der Implementierung sein. Wenn Sie externe Clients, mobile Apps und viele verschiedene Konsumenten haben, kann Stateless gerechtfertigt sein. Das richtige Kriterium ist das Client‑Verhalten und die Verantwortung für den Auth‑State, nicht das Alter der Technologie.

Fehler Nr. 2: Denken, dass POST /api/auth/login automatisch alle folgenden Anfragen authentifiziert.
Das ist die klassische Falle nach dem Umstieg auf STATELESS. Der Login‑Endpunkt ist der „Login als Ereignis“, aber das stateless‑Modell erfordert „Authentifizierbarkeit jeder geschützten Anfrage“. Wenn der Server keine Session speichert, kann er sich nicht „merken“, dass Sie sich vor fünf Sekunden eingeloggt haben. In diesem Moment ist es wichtig, nicht alles durch erneutes Aktivieren der Session zu „reparieren“, sondern das Modell ehrlich zu akzeptieren und weiter auszubauen.

Fehler Nr. 3: formLogin, httpBasic und stateless‑Modus gleichzeitig aktivieren „für alle Fälle“.
„Für alle Fälle“ bedeutet in der Sicherheit fast immer „wir schenken uns selbst Unvorhersehbarkeit“. Verschiedene Modelle beginnen zu konkurrieren: Ein Client bekommt einen Redirect, ein anderer einen Challenge, ein dritter einen JSON‑Fehler – und das alles auf demselben Endpunkt. Halten Sie in einem Übungsprojekt die Konfigurationen isoliert (Profile, Checkpoints), um die Kausalzusammenhänge zu verstehen.

Fehler Nr. 4: Die geschäftliche Autorisierung zurück in den Controller verlegen, weil „jetzt stateless ist, also muss es anders gemacht werden“.
Stateless bedeutet nicht, dass @PreAuthorize nicht mehr nötig ist. Im Gegenteil: Je mehr Clients Sie haben und je weniger Sie der Eingangsschicht vertrauen, desto wichtiger ist es, dass Geschäftsregeln im Service‑Layer leben. Stateless ändert den Authentifizierungstransport, hebt aber nicht die Owner‑Only‑Logik und das Role/Authority‑Modell auf.

Fehler Nr. 5: Glauben, dass HTTP Basic und stateless JSON Login dasselbe sind.
Beide Modelle sind vom Geist her stateless, aber sie haben unterschiedliche UX und unterschiedliche Verantwortungsbereiche. Basic zwingt den Client, jedes Mal das Passwort zu senden (und das ist selten für echte Benutzer‑Clients geeignet). Stateless JSON Login trennt normalerweise den „Moment der Passworteingabe“ von den wiederholten Anfragen. Beide im Kopf zu vermischen bedeutet, später eine mobile App unerwartet an das Passwort bei jeder Anfrage zu „binden“ und sich zu wundern, warum das eine schlechte Idee ist.

1
Aufgabe
Spring Security, Level 21, Lektion 3
Gesperrt
Vergleich von `formLogin` und `httpBasic` mittels Profilen
Vergleich von `formLogin` und `httpBasic` mittels Profilen
1
Aufgabe
Spring Security, Level 21, Lektion 3
Gesperrt
Vergleich von `httpBasic` und stateless JSON-Login
Vergleich von `httpBasic` und stateless JSON-Login
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION