CodeGym /Kurse /Spring Security /httpBasic ( ... ) und BasicAuthenticationFilter

httpBasic ( ... ) und BasicAuthenticationFilter

Spring Security
Level 10, Lektion 2
Verfügbar

1. Weg von HTTP Basic in Spring Security

Das Format Authorization: Basic ... ist bereits verständlich: Credentials kommen mit jeder Anfrage, und 401 sagt, dass der Server Sie nicht authentifizieren kann. Aber vorerst ist es nur eine Zeichenfolge. Jetzt bleibt noch die wichtigste technische Frage zu klären: Wie wird dieser Header überhaupt in den „aktuellen Benutzer“ in Spring Security umgewandelt?

Stellen Sie sich eine Situation aus unserem Projekt Secure Content Platform API vor. Wir haben einen geschützten Endpunkt /api/me, der den aktuellen Benutzer zurückgeben soll. Wir wissen bereits, dass der „aktuelle Benutzer“ in Spring im SecurityContext erscheint, und wir wissen, dass die Login/Passwort-Überprüfung nicht vom Controller, sondern von der standardmäßigen Username/Password-Pipeline (AuthenticationManager → AuthenticationProvider → UserDetailsService + PasswordEncoder) durchgeführt wird. Die Frage des Tages: Welches Glied in der Kette extrahiert Login und Passwort aus dem Authorization-Header und startet diese Pipeline?

Die Antwort — BasicAuthenticationFilter. Und damit alles klar wird, müssen wir den gesamten Weg der Anfrage vom Header bis zum SecurityContext genau so durchgehen, wie Spring Security es tut.

2. Aktivieren von httpBasic(...) in SecurityFilterChain

Man möchte glauben, dass HTTP Basic „irgendwo in Spring magisch aktiviert“ ist, aber unser Kurs handelt genau davon, Magie in verständliche Schalter zu verwandeln. Deshalb aktivieren wir Basic genauso explizit, wie wir formLogin aktiviert haben, und genauso explizit, wie wir authorizeHttpRequests schreiben. Das ist wichtig: Wenn Sie die Konfiguration in einem Monat sehen, sollten Sie auf einen Blick erkennen, welche Authentifizierungsmechanismen überhaupt verfügbar sind.

In unserem Projekt lebt die Konfiguration normalerweise im Paket com.example.securecontent.security.config. Angenommen, wir haben bereits Zugriffsregeln nach Zonen: öffentliche Endpunkte sind offen, /api/editor/** erfordert die Rolle EDITOR, /api/admin/** die Rolle ADMIN, und alles andere erfordert zumindest eine Authentifizierung. Dann sieht die Aktivierung von HTTP Basic wie folgt aus:

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 SecurityConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // 1) Zuerst beschreiben wir die Zugriffsregeln: wohin darf man, wohin nicht
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/public/**").permitAll() // öffentliche Zone
                .requestMatchers("/api/editor/**").hasRole("EDITOR") // Editor-Zone
                .requestMatchers("/api/admin/**").hasRole("ADMIN") // Admin-Zone
                .anyRequest().authenticated() // alles andere – nur für Eingeloggte
            )
            // 2) Dann aktivieren wir die Authentifizierungsmethode: wie genau man sich einloggt
            .httpBasic(Customizer.withDefaults()); // aktiviert BasicAuthenticationFilter und Challenge 401/WWW-Authenticate

        return http.build(); // Filterkette zusammenbauen
    }
}

Beachten Sie den Gedanken, den wir „sanft, aber beharrlich“ einprägen: httpBasic(...) ersetzt nicht authorizeHttpRequests. Es beantwortet nur die Frage „Wie authentifizieren?“, nicht „Wohin lassen?“. Die Berechtigungen (permitAll, authenticated, hasRole) bleiben Ihre Hauptsprache für „was erlaubt ist“.

Wenn Sie vergessen, .httpBasic(...) hinzuzufügen, können Sie immer noch perfekt geschriebene Request-Rules haben – aber der Basic-Client erhält nicht das erwartete Verhalten. Dies ist ein sehr typischer Fehler: „Ich habe den Header gesendet, warum funktioniert es nicht?“. Weil Sie den Mechanismus nicht aktiviert haben, der diesen Header liest.

3. Rolle von BasicAuthenticationFilter in der Kette

Jetzt kommt ein wichtiger Punkt: BasicAuthenticationFilter ist kein „separates Modul“ und kein „Controller in Spring“. Es ist ein Filter in Ihrer SecurityFilterChain, d.h. es arbeitet, bevor die Anfrage den DispatcherServlet und Ihre Controller erreicht. Und das erklärt 90% des Gefühls „warum wurde meine Methode im Controller nicht einmal aufgerufen“.

Vereinfacht gesagt, BasicAuthenticationFilter macht folgende Logik: Er betrachtet die eingehende HTTP-Anfrage, sucht nach dem Authorization-Header. Wenn der Header fehlt oder das Schema nicht Basic ist, lässt der Filter die Anfrage normalerweise einfach weiterlaufen (es gibt nichts zu authentifizieren). Wenn der Header vorhanden ist und wie Basic aussieht, versucht der Filter, daraus username und password zu extrahieren, ein UsernamePasswordAuthenticationToken-Objekt zu erstellen und es an den AuthenticationManager zu übergeben. Wenn die Überprüfung erfolgreich ist, legt der Filter das Ergebnis in den SecurityContext, und dann betrachtet die Anwendung die Anfrage als „von einem bestimmten Benutzer kommend“.

Um dies leichter mit dem zu verbinden, was Sie bereits in Tag 3 und Tag 4 gelernt haben, hier ein Diagramm (vereinfacht, aber ehrlich):

flowchart TD
    A["HTTP request"] --> B["SecurityFilterChain"]
    B --> C["BasicAuthenticationFilter"]
    C --> D{"Gibt es Authorization: Basic ... ?"}
    D -- "nein" --> E["Anfrage geht als anonymous weiter"]
    E --> F["Authorization rules: authenticated? hasRole?"]
    F --> G["Wenn Login benötigt: 401 + WWW-Authenticate"]

    D -- "ja" --> H["Dekodiere Base64 -> username:password"]
    H --> I["UsernamePasswordAuthenticationToken"]
    I --> J["AuthenticationManager.authenticate(...)"]
    J --> K["DaoAuthenticationProvider"]
    K --> L["UserDetailsService + PasswordEncoder"]
    L --> M["Authentication (authenticated=true)"]
    M --> N["SecurityContext"]
    N --> O["Controller erhält Principal/Authentication"]

Dieses Diagramm ist wichtig, weil es zwei verschiedene Zweige zeigt: den Zweig „keine Credentials“ und den Zweig „Credentials vorhanden“. Und in beiden Fällen bleiben die Request-Rules (authenticated(), hasRole(...)) der „Wächter an der Tür“, der entscheidet, was als nächstes zu tun ist.

4. Analyse von Authorization in UsernamePasswordAuthenticationToken

Nach Vorlesung 2 sollten Sie bereits das Gefühl haben, dass der Basic-Header nur eine Zeichenfolge ist. Jetzt machen wir den nächsten Schritt: wir erklären, wie Spring diese Zeichenfolge in ein Objektmodell umwandelt, mit dem die Security-Engine arbeiten kann. In Spring Security dreht sich alles um das Objekt Authentication, und Basic ist keine Ausnahme: es erstellt einfach eine Authentication aus dem Header.

Am Eingang haben wir eine HTTP-Anfrage mit einem Header der Form:

Authorization: Basic ZWRpdG9yOnNlY3JldA==

Als Nächstes führt der Filter zwei aufeinanderfolgende Dinge aus. Zuerst prüft er, ob das Schema tatsächlich Basic ist. Dann dekodiert er Base64, erhält die ursprüngliche Zeichenfolge username:password und teilt sie am Doppelpunkt in zwei Teile. Danach wird ein UsernamePasswordAuthenticationToken erstellt, der der Standardcontainer für die Username/Password-Authentifizierung ist.

Sie haben diese Klasse bereits in der Erklärung von DaoAuthenticationProvider gesehen. Die gute Nachricht: In HTTP Basic gibt es keine „andere Prüfwelt“. Es ist immer noch dieselbe Pipeline, nur die Quelle von username/password ist nicht das HTML-Formular, sondern der Header.

Damit Sie dies live sehen können, schauen Sie sich ein minimales Codebeispiel an, „wie der Filter es machen würde“ (dies ist nicht etwas, das Sie in Ihr Projekt einfügen; es ist eine Lehrveranschaulichung):

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

// Was der Filter letztendlich aus dem Header erhalten möchte: username und raw password
String username = "editor";
String password = "secret";

// Zu diesem Zeitpunkt ist das Token noch NICHT bestätigt (Credentials wurden nur in das Objekt übertragen)
UsernamePasswordAuthenticationToken authRequest =
        UsernamePasswordAuthenticationToken.unauthenticated(username, password);

Das Schlüsselwort hier ist unauthenticated(...). Zu diesem Zeitpunkt hat das Token einen principal (username) und credentials (password), ist aber noch nicht bestätigt. Dies ist eine wichtige Logik von Spring Security: „Solange nicht geprüft, vertrauen wir niemandem“.

Dann geht dieses Objekt zum AuthenticationManager, und dort wird entschieden, ob es authenticated=true wird und welche roles/authorities es haben wird.

5. AuthenticationManager und DaoAuthenticationProvider

Jetzt fügen wir zwei Blöcke des Kurses zu einem Bild zusammen: die Authentifizierungsarchitektur (Ebene 4) und HTTP Basic (Ebene 10). Sehr oft denken Anfänger, dass „Form-Login ein Mechanismus, Basic ein anderer Mechanismus ist, also muss auch die Passwortprüfung woanders sein“. Tatsächlich unterscheidet sich nur „wie wir Login und Passwort erhalten haben“, und danach läuft alles auf demselben Weg.

BasicAuthenticationFilter nimmt UsernamePasswordAuthenticationToken und führt in etwa folgenden Aufruf aus:

import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;

// In der Realität wird AuthenticationManager von Spring injiziert/konfiguriert.
// Hier null – nur um zu betonen: normalerweise erstellt man ihn NICHT von Hand.
AuthenticationManager authenticationManager = null;

 // Übergebe die Authentifizierungsanfrage an die „Engine“ von Spring Security
Authentication result = authenticationManager.authenticate(authRequest);

// Wenn alles ok ist, wird result authenticated=true sein und mit gefüllten authorities

In einer realen Anwendung erstellen Sie AuthenticationManager nicht von Hand. Spring setzt ihn aus einer Reihe von AuthenticationProviders zusammen, und die typische Implementierung ist ProviderManager. Der für uns wichtigste Provider in dieser Phase ist DaoAuthenticationProvider, weil er Benutzer über UserDetailsService und Passwort über PasswordEncoder überprüfen kann.

Das Prüfschema sieht so aus: der Provider fragt den Benutzer nach username (z.B. aus InMemoryUserDetailsManager), erhält UserDetails mit passwordHash (encodiertes Passwort), dann vergleicht er über PasswordEncoder.matches(...) das Passwort aus dem Header mit dem im System gespeicherten. Wenn es übereinstimmt, erstellt er eine neue Authentication, die bereits authorities (z.B. ROLE_EDITOR) und das Flag „authentifiziert“ enthält.

Dies ist „dieselbe Mechanik“, die Sie kennengelernt haben, als Sie DaoAuthenticationProvider besprochen haben, und das ist eine sehr gute Nachricht: Ihr Gehirn muss nicht zwei verschiedene „Motoren“ im Kopf haben. Ein Motor, zwei verschiedene Eingänge: Formular oder Header.

Um dies mit unserem Projekt zu verbinden, erinnere ich daran: In-Memory-Benutzer haben wir so erstellt, dass das Passwort encoded ist, nicht roh. Und diese Regel verschwindet nicht, wenn Sie Basic aktivieren. Beispiel:

import org.springframework.context.annotation.Bean;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.core.userdetails.UserDetailsService;

@Bean
UserDetailsService userDetailsService(PasswordEncoder encoder) {
    // Speichern Passwort in encoded-Form, auch wenn es in-memory und ein "Lehr"-Beispiel ist
    UserDetails editor = User.withUsername("editor")
            .password(encoder.encode("secret")) // raw password -> encoded password
            .roles("EDITOR") // Spring wandelt automatisch in ROLE_EDITOR um
            .build();

    // InMemoryUserDetailsManager gibt diesen Benutzer an DaoAuthenticationProvider weiter
    return new InMemoryUserDetailsManager(editor);
}

HTTP Basic rechtfertigt nicht „es ist ja nur ein Header, legen wir das Passwort direkt als String ab“. Nein. Das Passwort wird in encoded-Form gespeichert, und raw password existiert nur „für einen Moment“ während der Überprüfung.

6. SecurityContext: was der Controller sieht

Wenn die Authentifizierung erfolgreich war, muss Spring Security die Hauptsache tun: die aktuelle Anfrage als authentifiziert deklarieren. Dies geschieht über SecurityContext. Einfach ausgedrückt ist SecurityContext die „Tasche“ des aktuellen Verarbeitungs-Threads, in der die Information „wer gerade die Anfrage stellt“ gespeichert ist.

Und hier passiert ein sehr angenehmer Punkt: Ihr Controller muss überhaupt nicht wissen, auf welche Weise der Benutzer authentifiziert wurde. Es kann formLogin sein, es kann httpBasic sein, und der Controller arbeitet dennoch mit dem bereits fertigen Benutzerbild, weil die Filter vor ihm ihre Arbeit gemacht haben.

In unserem Projekt gibt es den Endpunkt /api/me. Betrachten wir eine einfache Variante, die den Benutzernamen anzeigt:

package com.example.securecontent.me;

import java.security.Principal;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
class MeController {

    @GetMapping("/api/me")
    String me(Principal principal) {
        // Principal wird nur gefüllt, wenn die Anfrage authentifiziert ist
        return principal.getName(); // username aus SecurityContext
    }
}

Wenn die Anfrage mit einem korrekten Basic-Header kommt und der Benutzer das Recht auf diesen Endpunkt hat (nach unseren Regeln einfach „authentifiziert zu sein“), dann wird principal nicht null sein und principal.getName() den username zurückgeben.

Wenn man zu Lernzwecken mehr Details sehen möchte, ist es manchmal bequem, Authentication entgegenzunehmen und authorities anzuschauen:

package com.example.securecontent.me;

import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
class MeDebugController {

    @GetMapping("/api/me/debug-auth")
    String debugAuth(Authentication authentication) {
        // Authentication – eine detailliertere Version des aktuellen Benutzers
        return authentication.getName() + " " + authentication.getAuthorities();
    }
}

Dies ist eine einfache „Lernlupe“: Sie sehen anschaulich, was Spring Security tatsächlich in den Kontext gelegt hat. Und ja, wenn alles richtig konfiguriert ist, zeigt authentication.getAuthorities() so etwas wie [ROLE_EDITOR] (das genaue Format hängt von der Implementierung und toString() ab).

Der Hauptgedanke dieses Abschnitts: Bei erfolgreicher Authentifizierung gibt der Basic-Filter nicht „selbst eine Antwort zurück“. Er füllt lediglich den SecurityContext, und dann läuft alles wie gewohnt: Zugriffsregeln werden überprüft, der Controller wird aufgerufen, der Service erledigt seine Arbeit.

7. Fehlgeschlagene Authentifizierung: 401 und WWW-Authenticate

Die 401 selbst ist hier keine Neuigkeit. Interessanter ist etwas anderes: Innerhalb von Spring Security kann sie auf zwei verschiedenen Wegen entstehen. Von außen enden beide normalerweise gleich — 401 Unauthorized und WWW-Authenticate — aber auf dem Weg zu dieser Entscheidung verläuft die Anfrage unterschiedlich.

Im ersten Fall kommen Sie ohne Authorization-Header. Dann konnte BasicAuthenticationFilter nichts authentifizieren, die Anfrage geht als anonymous weiter, und bei der Prüfung authenticated() oder hasRole(...) stellt sich heraus, dass ein Login dennoch erforderlich ist.

Im zweiten Fall ist der Header vorhanden, aber die Credentials sind falsch. Dann schafft es BasicAuthenticationFilter, ein UsernamePasswordAuthenticationToken zu erstellen, übergibt es an AuthenticationManager, und das Problem tritt bereits bei der Prüfung des Benutzers oder des Passworts auf. Für den Client von außen ist es trotzdem 401, aber für das Debugging ist der Unterschied wesentlich: im einen Fall sind Credentials gar nicht angekommen, im anderen sind sie angekommen, haben aber die Auth-Pipeline nicht bestanden.

Um dies selbst zu sehen, reichen zwei Anfragen mit curl. Die erste – ohne Header:

curl -i http://localhost:8080/api/me

Normalerweise sehen Sie HTTP/1.1 401 und WWW-Authenticate. Die zweite – mit falschem Passwort:

curl -i -u editor:WRONG http://localhost:8080/api/me

Und wieder 401, aber mit dem Verständnis: Credentials waren vorhanden, haben aber die Prüfung nicht bestanden.

Und es gibt die dritte Anfrage – die glückliche:

curl -i -u editor:secret http://localhost:8080/api/me

Und hier sehen Sie 200 OK und den Antwortkörper mit username.

Beachten Sie eine wichtige Sache: Der Controller kann in erfolglosen Szenarien überhaupt nicht aufgerufen werden. Und das ist normal. Die Security-Entscheidung wird auf Filterebene getroffen, also früher. Daher hilft es nicht immer, „einen Breakpoint in den Controller zu setzen“: manchmal erreicht die Anfrage MVC nicht.

8. Debug-Logs: Wie man den Basic-Fluss sieht

Es ist sehr leicht, ein Schema zu lesen, zu nicken und zu vergessen. Daher ist es nützlich, eine kleine Überprüfung zu machen: „Sehe ich wirklich, was passiert?“. Wir haben bereits gelernt, Debug-Logs der Filterkette zu lesen, und HTTP Basic ist eine großartige Gelegenheit, diese Fähigkeit ohne unnötige neue Themen zu wiederholen.

Die einfachste Möglichkeit – vorübergehend Debug-Logging für Spring Security in application.yml aktivieren:

logging:
  level:
    # Aktiviert detaillierte Logs für Filter und Authentifizierung
    org.springframework.security: DEBUG

Dann machen Sie eine Anfrage ohne Header und eine mit Header, und in den Logs sehen Sie, wie Spring Filter auswählt und wie sich der „Authentifizierungszustand“ ändert.

Der Sinn der Übung ist nicht, alle Filternamen auswendig zu lernen (es gibt viele, und das ist normal). Der Sinn ist, dass Sie lernen, drei praktische Fragen zu beantworten: Hat der Filter den Header gesehen, hat er versucht zu authentifizieren, und wie endete der Versuch. Das sind genau die Fragen, die Sie sich bei jedem „Warum habe ich 401?“ stellen werden.

9. Typische Fehler

Fehler Nr. 1: „Ich habe den Header gesendet, also muss Spring mich durchlassen.“
Anfänger erwarten manchmal, dass das Vorhandensein von Authorization: Basic ... automatisch Zugang zum Endpunkt bedeutet. Tatsächlich ist der Header nur für den Authentifizierungsversuch zuständig, und dann greifen Ihre Regeln authorizeHttpRequests. Sie können erfolgreich authentifiziert sein, aber dennoch eine Zugriffsverweigerung wegen hasRole("ADMIN") in der Admin-Zone erhalten.

Fehler Nr. 2: .httpBasic(...) in der Konfiguration vergessen, aber sicher sein, dass Basic funktioniert.
Das ist ein Klassiker. Besonders wenn Sie zuvor formLogin konfiguriert haben und gesehen haben, dass „irgendwas mit Login“ in der Anwendung bereits vorhanden ist. Aber Spring Security ist nicht verpflichtet, alle Mechanismen sofort zu aktivieren. Wenn in SecurityFilterChain kein httpBasic(Customizer.withDefaults()) vorhanden ist, wird die Basic-Authentifizierung möglicherweise nicht wie erwartet aktiviert, und der Header ist nur eine nutzlose Zeichenfolge in der Anfrage.

Fehler Nr. 3: Versuch, das Passwort manuell im Controller zu überprüfen.
Manchmal will man es „schnell und einfach“ machen: Authorization im Controller entgegennehmen, Base64 dekodieren, Passwort als String vergleichen und 200 zurückgeben. Das klingt nach einem Plan, bis man bedenkt, dass man damit AuthenticationManager, DaoAuthenticationProvider, UserDetailsService, PasswordEncoder und den ganzen Sinn von Spring Security als Infrastruktur über Bord wirft. Das Ergebnis ist ein fragiles selbstgeschriebenes Security-Stück, das bei der ersten neuen Rolle, beim Logging oder bei der Notwendigkeit einheitlicher Fehler kaputtgeht.

Fehler Nr. 4: In-Memory-Passwort „wie es ist“ speichern, ohne PasswordEncoder.
HTTP Basic macht das Passwort nicht weniger „echt“. Wenn Sie das Passwort als String ablegen und den Encoder umgehen, etablieren Sie ein falsches Modell und riskieren, es später auf den DB-Zweig zu übertragen. Die richtige Gewohnheit bleibt: Passwort wird encoded gespeichert, und raw password existiert nur als Eingabe für PasswordEncoder.matches(...).

Fehler Nr. 5: Vollständiges Loggen des Authorization-Headers.
Es scheint: „Ich bin doch in der Entwicklung, ich muss debuggen“. Aber Authorization: Basic ... enthält wiederherstellbare Credentials. Eines Tages vergessen Sie, das Logging auszuschalten, laden die Logs irgendwo in einen zentralen Sammler hoch, und das ergibt eine kleine Lehrleckage mit großen Konsequenzen. Wenn Sie loggen müssen, loggen Sie die Tatsache, dass der Header vorhanden ist, aber nicht seinen Inhalt.

1
Aufgabe
Spring Security, Level 10, Lektion 2
Gesperrt
Wer nach HTTP Basic in den SecurityContext gelangt
Wer nach HTTP Basic in den SecurityContext gelangt
1
Aufgabe
Spring Security, Level 10, Lektion 2
Gesperrt
Offener Endpunkt und aktueller Benutzer in einem Controller
Offener Endpunkt und aktueller Benutzer in einem Controller
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION