1. Vergleich der Authentifizierungsmodelle
Wenn bereits klar ist, wie Basic zum SecurityContext gelangt, stellt sich die nächste Frage von selbst: Worin unterscheidet sich dieses Modell eigentlich von formLogin + HttpSession in derselben Anwendung? Es ist sehr leicht, in die Falle zu tappen „ich habe ein Schema verstanden — also ist es immer richtig“. Besonders, wenn das Schema beim ersten Mal funktioniert hat und in den Logs nicht einmal roter Text war (was nach Backend-Maßstäben bereits ein Fest ist). Aber formLogin + HttpSession und HTTP Basic lösen dieselbe Aufgabe — einen Request zu authentifizieren, tun dies jedoch so unterschiedlich, dass die Wahl UX, Debugging und die Art und Weise beeinflusst, wie genau der Client mit der API kommuniziert.
Der Vergleich in derselben Anwendung ist aus genau zwei Gründen nützlich. Erstens hören Sie auf, Spring Security als eine Menge magischer Annotationen zu betrachten, und beginnen, den „Vertrag“ zwischen Client und Server zu sehen: was genau der Client sendet, wie der Server antwortet und was zwischen den Anfragen gespeichert wird. Zweitens beginnen Sie noch sicherer, Authentication („Wer bist du?“) von Authorization („Was darfst du?“) zu unterscheiden, da die Autorisierungsregeln dieselben bleiben — nur die Art des Logins ändert sich.
2. Was sich in der Anwendung nicht ändert
Der wichtigste Punkt dieses Vergleichs klingt fast langweilig: Beim Ersetzen von formLogin durch HTTP Basic schreiben Sie in der Regel keinen Geschäftscode neu. Die Controller sollten nicht „wissen“, ob Sie sich über ein Formular oder einen Header angemeldet haben. Wenn ein Controller anfängt zu fragen: „Sind Sie sicher, dass Sie mit formLogin gekommen sind?“ — das ist ungefähr so, als würde ein Busfahrer prüfen, ob Sie Ihr Ticket am Schalter oder in der App gekauft haben. Dem Bus ist es egal, Hauptsache das Ticket ist gültig.
In unserem Projekt gibt es klare Zonen: /api/public/** ist für alle offen, /api/me erfordert Authentifizierung, /api/editor/** und /api/admin/** erfordern eine Rolle. Diese Regeln leben in SecurityFilterChain, und im Controller ist für uns nur der aktuelle Benutzer (Principal) wichtig, der nach dem Durchlaufen der Sicherheitskette bereits vorhanden ist.
Hier ist ein minimales Beispiel einer „persönlichen Zone“ — der Endpoint /api/me. Er funktioniert sowohl mit formLogin als auch mit HTTP Basic auf die gleiche Weise, da Spring Security in beiden Fällen letztendlich den Principal füllt.
package com.example.securecontent.profile;
import java.security.Principal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MeController {
@GetMapping("/api/me")
public String me(Principal principal) {
// Auf diesen Endpoint lassen wir nur authentifizierte Benutzer zu,
// daher ist principal hier erwartungsgemäß nicht null.
// (Falls der Zugriff versehentlich geöffnet wird, fangen Sie eine NPE und merken schnell, dass die Regeln gebrochen sind.)
return principal.getName(); // Gibt „wer ich bin“ zurück — den Namen des aktuellen Benutzers aus dem SecurityContext
}
}
Wichtiger Gedanke: Wir schreiben „Login“ nicht von Hand. Wir schreiben einen Endpoint, der nachdem die Sicherheitsebene ihre Arbeit bereits erledigt hat und „wer ich bin“ in den Anfragekontext gelegt hat, funktioniert.
3. formLogin + HttpSession: das Gefühl „ich bin bereits eingeloggt“
formLogin eignet sich gut als didaktische Brücke, da es visuell ist: Der Browser zeigt tatsächlich eine Login-Seite an, Sie geben Benutzername/Passwort ein, und dann hat man das Gefühl, dass „das System Sie sich merkt“. Dieses Gefühl ist weder Magie noch Telepathie des Servers, sondern ein ganz konkreter Mechanismus: Nach erfolgreicher Authentifizierung erstellt (oder aktualisiert) der Server eine Session und sendet dem Client das Cookie JSESSIONID. Der Browser hängt dieses Cookie dann an alle folgenden Anfragen an, und der Server findet daran Ihren Zustand.
Wenn man es ohne Philosophie ausdrückt: formLogin + HttpSession ist ein Modell, bei dem Credentials einmal (beim Login) übertragen werden und der Client dann ein „Armband“ (Session-ID) mit sich trägt, das es dem Server ermöglicht, den SecurityContext wiederherzustellen. Innerhalb von Spring Security sieht das so aus: „Authentifizierung ist passiert, Kontext wurde gespeichert, weitere Requests werden einfach erkannt“.
Am deutlichsten wird dies in der Sequenz der Anfragen:
sequenceDiagram
%% formLogin: einmal anmelden, danach läuft das "Armband" (JSESSIONID) automatisch mit uns mit
participant B as "Browser"
participant S as "Secure Content Platform API"
B->>S: "GET /api/me (anonym)"
S-->>B: "302 Weiterleitung zu /login"
B->>S: "POST /login (Benutzername + Passwort)"
S-->>B: "Set-Cookie: JSESSIONID=..."
B->>S: "GET /api/me + Cookie: JSESSIONID=..."
S-->>B: "200 OK (ich = Benutzername)"
Und hier ist es wichtig, nichts zu verwechseln: Das „Armband“ (JSESSIONID) ist nicht Ihr Benutzer und nicht Ihre Rolle. Es ist lediglich ein Schlüssel, über den der Server den gespeicherten Sicherheitskontext findet.
4. HTTP Basic: „Jeder Request kommt mit einem Pass“
HTTP Basic ähnelt einem sehr strengen Zugangssystem: Sie zeigen Ihren Pass nicht einmal beim Betreten des Gebäudes vor, sondern legen ihn jedes Mal vor, wenn Sie ein Büro betreten. Nicht, weil das System böswillig ist, sondern weil das Modell so ist: Die Credentials werden bei jedem geschützten Request im Header Authorization mitgeliefert.
In den vorherigen Lektionen haben wir bereits das Format des Headers behandelt, daher ist für uns hier das „Verhaltensmodell“ wichtig. Wenn ein Request keinen Authorization: Basic ...-Header hat, kann der Server den Request nicht authentifizieren und antwortet mit „Ich brauche Credentials“. Dies wird durch 401 und den Header WWW-Authenticate ausgedrückt, was für den Client bedeutet: „Versuch es noch einmal, aber diesmal mit den Daten“.
Eine typische Sequenz sieht so aus:
sequenceDiagram
%% httpBasic: Authentifizierung "kommt" in jeder Anfrage im Authorization-Header mit
participant C as "API Client (curl/Postman)"
participant S as "Secure Content Platform API"
C->>S: "GET /api/me (kein Authorization)"
S-->>C: "401 + WWW-Authenticate: Basic ..."
C->>S: "GET /api/me + Authorization: Basic ..."
S-->>C: "200 OK (ich = Benutzername)"
C->>S: "GET /api/admin/users + Authorization: Basic ..."
S-->>C: "403 Forbidden (falls nicht ADMIN)"
Beachten Sie die letzte Zeile: HTTP Basic beantwortet nur die Frage „Wer bist du?“. Die Frage „Was darfst du?“ wird weiterhin von Ihren Regeln hasRole("ADMIN"), authenticated() usw. gelöst.
5. Zwei Varianten von SecurityFilterChain
Es ist sehr verlockend zu denken, dass die Umstellung auf HTTP Basic bedeutet, „das halbe Projekt umzuschreiben“. In der Praxis ist es in einem Übungsprojekt oft buchstäblich eine Zeile in der Konfiguration. Unten sind zwei reine Vergleichsvarianten derselben Zugriffskarte: Sie dienen genau dem Vergleich des Client-Verhaltens. In einer Produktionskonfiguration wählen Sie einen Authentifizierungsmechanismus aus, anstatt zwei identische Ketten „für alle Fälle“ nebeneinander zu halten.
Unten sind zwei Varianten derselben Konfiguration, bei der die Zugriffsregeln identisch sind, sich jedoch nur der Authentifizierungsmechanismus unterscheidet.
Variante A: Browserorientierter Login über formLogin.
package com.example.securecontent.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfigFormLogin {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// Zugriffsregeln (authorization): Wer wohin darf
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll() // öffentliche Zone
.requestMatchers("/api/admin/**").hasRole("ADMIN") // Zone nur für ADMIN
.anyRequest().authenticated() // alles andere — nur nach Login
)
// Authentifizierungsmechanismus (authentication): Login über Formular + Session (JSESSIONID)
.formLogin(Customizer.withDefaults())
.build();
}
}
Variante B: API-orientierter Login über HTTP Basic.
package com.example.securecontent.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfigHttpBasic {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// Dieselben Zugriffsregeln (authorization) — ändern sich überhaupt nicht
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll() // öffentliche Zone
.requestMatchers("/api/admin/**").hasRole("ADMIN") // Zone nur für ADMIN
.anyRequest().authenticated() // alles andere — nur nach Login
)
// Nur der Authentifizierungsmechanismus ändert sich: jetzt erwarten wir Authorization: Basic ...
.httpBasic(Customizer.withDefaults())
.build();
}
}
Zum Vergleich ist es nützlich, buchstäblich eine Aktion durchzuführen: vorübergehend .formLogin(...) durch .httpBasic(...) zu ersetzen und zu beobachten, wie sich der Vertrag mit dem Client bei denselben Endpoints ändert.
6. Clients: Browser und curl/Postman
Wenn man Auth-Mechanismen vergleicht, vergleichen Anfänger oft „auf Annotationsebene“: .formLogin() oder .httpBasic() eingeschaltet — und fertig. Aber die ehrlichste Vergleichsmethode ist, aus der Perspektive des Clients zu schauen. Der Browser mag Weiterleitungen und Login-Seiten, API-Clients mögen Statuscodes und Header. Und Spring Security „spricht“ auch unterschiedlich mit ihnen.
Unten ist eine kompakte Tabelle, die hilft, die Eindrücke nicht zu verwechseln:
| Frage | formLogin + HttpSession | HTTP Basic |
|---|---|---|
| „Wie beweise ich zum ersten Mal, wer ich bin?“ | Durch den Login-Flow (Login-Formular) | Durch den Authorization-Header |
| „Was sende ich bei jeder Anfrage nach dem Login?“ | Normalerweise das Cookie JSESSIONID (der Browser erledigt das selbst) | Erneut Authorization: Basic ... |
| „Was passiert, wenn ich nicht authentifiziert bin?“ | Oft eine Weiterleitung auf /login (besonders im Browser sichtbar) | 401 + WWW-Authenticate |
| „Wie testet man mit curl?“ | Unbequemer: man muss die Browser-Semantik nachbilden | Sehr praktisch: curl -u user:pass ... |
| „Wie testet man über den Browser?“ | Sehr natürlich | Es kann ein systemeigenes Basic-Fenster erscheinen (und das ist nicht immer angenehm) |
Um den Unterschied selbst zu spüren, reichen zwei curl-Befehle für Basic. Der erste — ohne Credentials, um 401 zu sehen:
# Anfrage ohne Authorization-Header — Server kann Client nicht authentifizieren
curl -i http://localhost:8080/api/me
Der zweite — mit Credentials, um 200 und die Antwort des Controllers zu sehen:
# -u user:password zwingt curl, Authorization: Basic ... zu senden
curl -i -u user:password http://localhost:8080/api/me
Bei formLogin machen Sie es normalerweise umgekehrt: Sie loggen sich über den Browser ein und gehen dann einfach zu /api/me und anderen Endpoints, ohne über Header nachzudenken, da der Browser selbst das Cookie anfügt. Das ist der entscheidende UX-Effekt einer Session: Sie sind „eingeloggt“, und die Interaktion erscheint als eine einzige kontinuierliche Sitzung.
7. Trade-offs: Bequemlichkeit, Sicherheit, Betrieb
Der Vergleich von formLogin und HTTP Basic kann leicht zu einem Religionskrieg werden: „Was ist moderner?“. Aber unser Kurs lehrt ausdrücklich etwas anderes: Wir wählen den Mechanismus passend zum Client und zum Szenario, nicht nach Mode. In einem Übungsprojekt ist dies besonders wichtig: Sie müssen die Mechanik sehen, nicht nur „den richtigen Knopf merken“.
Wenn wir ehrlich sind, passt formLogin + HttpSession perfekt zum Browser-Modell. Der Browser kann mit Cookies umgehen, kann Zustand zwischen Anfragen übertragen, kann eine „Sitzung“ aufrechterhalten. Aus Serversicht gewinnen Sie ebenfalls: Das Passwort wird beim Login überprüft, und danach stellt der Server meistens einfach den Kontext über die Session-ID wieder her und führt die Autorisierung durch. Das erzeugt ein Gefühl von „schnell und flüssig“.
HTTP Basic hingegen ist hervorragend als minimale Baseline für einen API-Client. Es macht Authentication maximal sichtbar: Header, Schema, Daten. Für Postman und curl ist dies das ideale Format, da sie bereits von Headern „leben“. Es wird manchmal als staatenlos (stateless) bezeichnet, einfach weil die Credentials mit jeder Anfrage kommen. Aber es ist noch kein Token-Modell: Der Client transportiert immer noch das Passwort, keinen kurzlebigen Access Token.
Es hat jedoch einen offensichtlichen Preis: Sie übertragen wiederherstellbare Credentials in jeder Anfrage. Das bedeutet, dass Disziplin in Bezug auf HTTPS und Logging nicht nur „gute Praxis“ ist, sondern eine absolute Pflicht-Hygiene, sonst verursachen Sie versehentlich selbst ein Leck.
Man kann es noch einfacher sagen: formLogin ist „Ich zeige meine Dokumente einmal und erhalte ein Armband“, HTTP Basic ist „Ich zeige meine Dokumente jedes Mal“. Und beide Ansätze können technisch angemessen sein, wenn der Kontext stimmt.
Nach einem solchen Vergleich lautet die Frage nicht mehr „Was ist generell besser“, sondern „Für welchen Client und welches Szenario ist welcher Mechanismus passend“. Und genau dort zeigen sich bei Basic nicht nur Vorteile, sondern auch durchaus harte Grenzen.
8. Typische Fehler: formLogin und HTTP Basic
Fehler Nr. 1: Die Erwartung, dass .httpBasic() „Zugriff erlaubt“.
„httpBasic aktiviert“ bedeutet „eine Möglichkeit hinzugefügt, eine Anfrage zu authentifizieren“, nicht „Schlösser von allen Türen entfernt“. Die Regeln hasRole("ADMIN") und authenticated() verschwinden nicht: Ein Benutzer ohne die erforderliche Rolle erhält weiterhin 403 auf /api/admin/**.
Fehler Nr. 2: Die Erwartung „einmal eingeloggt und vergessen“ von HTTP Basic.
Im Basic-Modell hängt der Client entweder Authorization: Basic ... an jede Anfrage an oder gilt als nicht authentifiziert. Wenn Sie in Postman eine Anfrage mit Autorisierung gesendet haben und bei der nächsten „vergessen, dieselbe Autorisierung auszuwählen“, sehen Sie 401 — und das ist nicht „Server kaputt“, sondern „Client kam ohne Unterlagen“.
Fehler Nr. 3: Protokollieren des Headers Authorization zum Debuggen.
Das ist verlockend (er ist ja „so verständlich“), aber genau das ist der Fall, in dem Debuggen zu einem Leck wird: Logs leben lange und wandern von Hand zu Hand. Mit wiederherstellbaren Anmeldeinformationen in jeder Anfrage kann eine solche „kleine“ Gewohnheit zu einem sehr großen Problem werden.
Fehler Nr. 4: Übertragen von Browser-Erwartungen auf HTTP Basic.
Zum Beispiel eine schöne Login-Seite zu erwarten. Basic geht es nicht um Seiten, sondern um Header (401, WWW-Authenticate, Authorization). Wenn Sie einen „UI-Login“ benötigen — das ist bereits das Terrain von formLogin (oder eines anderen interaktiven Flows).
Fehler Nr. 5: Übertragen von Erwartungen von HTTP Basic auf formLogin beim Testen.
Zu erwarten, dass formLogin in curl „wie Basic“ bequem ist, ist ebenfalls seltsam. formLogin ist ein browserorientierter Mechanismus, er muss nicht angenehm für einen Konsolenclient sein: dort gibt es Weiterleitungen, Cookies und die Semantik einer „Sitzung“, nicht „ein Header — und fertig“.
GO TO FULL VERSION