CodeGym /Kurse /Spring Security /Spring Security im Request-Lifecycle

Spring Security im Request-Lifecycle

Spring Security
Level 3, Lektion 0
Verfügbar

1. Das Modell Endpoint = Controller

Eine URL bedeutet noch nicht, dass der Request die Controller-Methode erreicht hat. Fast jeder Anfänger – und ehrlich gesagt auch viele erfahrene Entwickler – denkt zunächst so: Es gibt eine URL, also gibt es eine Controller-Methode, also hat der Request „die Methode erreicht" und dort ist „etwas passiert". Das ist bequem und verständlich, aber auf einem echten Server gibt es zwischen „HTTP-Request ist eingetroffen" und „Controller-Methode wurde ausgeführt" einen ganzen Korridor an Infrastruktur. Und genau in diesem Korridor lebt die Sicherheit.

Stell dir vor, der Controller ist das Sprechzimmer eines Arztes. Der Patient (HTTP-Request) teleportiert sich nicht direkt zum Arzt. Zunächst betritt er das Gebäude (die Anwendung), passiert die Rezeption, den Sicherheitsdienst, vielleicht die Garderobe (Filter) – und erst dann gelangt er ins Sprechzimmer (Controller). Wenn der Sicherheitsdienst entschieden hat, dass kein Einlass möglich ist (kein Ausweis, falsche Uhrzeit, falsche Rolle), erfährt das Sprechzimmer des Arztes nicht einmal, dass jemand gekommen war. Das ist kein „Fehler des Sprechzimmers". Das ist ein korrekt funktionierender Eingang des Gebäudes.

Genau deshalb betonen wir im Spring-Security-Kurs so beharrlich: „Suche das Problem nicht nur im Controller." Sehr oft wird der Controller schlicht nicht aufgerufen, weil die Entscheidung früher getroffen wurde.

Servlet-Container: der Erste in der Verarbeitung

Die Worte Servlet-Container klingen wie etwas aus einem Lovecraft-Roman, aber in der Praxis ist das eine sehr bodenständige Sache. Ein Servlet-Container ist eine Umgebung, die eingehende HTTP-Requests entgegennimmt und deren Verarbeitung nach den Regeln der Servlet-API startet. In einer Spring-Boot-Anwendung ist das üblicherweise ein eingebetteter Server (z.B. Tomcat), der zusammen mit der Anwendung startet und beginnt, einen Port zu lauschen.

Wenn du Spring Boot startest, passiert eine wichtige „Magie" (genauer gesagt: Ingenieurleistung): Der Container wird hochgefahren, Servlets und Filter werden registriert, der Spring-ApplicationContext wird erstellt, und der Container lernt, den Spring-Teil der Anwendung aufzurufen. Aber das Wichtigste zu merken ist: als Erstes sieht der Container den Request, nicht Spring MVC und nicht dein Controller.

Vereinfacht sieht die Verarbeitung eines Requests so aus:

1) Request trifft im Container ein;
2) Der Container schickt den Request durch die Servlet-Filter-Chain;
3) Der Container übergibt den Request an einen der Servlets (in unserem Fall an den DispatcherServlet von Spring MVC);
4) Spring MVC findet den Controller und ruft die Methode auf;
5) Die Antwort geht den gleichen Weg zurück, nur in umgekehrter Richtung.

Und genau in Punkt Nr. 2 – in der „Filter-Chain" – lebt ein bedeutender Teil von Spring Security. Daher ist die Frage „Wo befindet sich Spring Security?" keine philosophische, sondern eine sehr praktische: An welchem Punkt der Request-Verarbeitung steht es.

2. Servlet-Filter: ein Rahmen um den Request

Ein Filter in der Servlet-API ist ein Element der Pipeline, das Code ausführen kann bevor der Request zum Servlet gelangt, und nachdem das Servlet die Arbeit abgeschlossen hat. Dieser „Wrapper"-Effekt ist sehr wichtig: Ein Filter kann nicht nur loggen, sondern auch Request/Response modifizieren – und manchmal die Verarbeitung abschließen, bevor überhaupt jemand bis zum MVC kommt.

Einen Filter kann man sich gut als „Rahmen" vorstellen, der alles Nachfolgende umschließt:

- „Davor" – Vorbereitung, Prüfungen, Logging;
- chain.doFilter(...) – Weitergabe;
- „Danach" – Abschluss, Metriken, abschließende Header, Audit.

Ein minimaler Lernfilter (im Projekt kann man ihn vorübergehend z.B. in com.example.securecontent.common.web ablegen) sieht so aus:

package com.example.securecontent.common.web;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // Anweisung: Filter so früh wie möglich ausführen, um die Ausgabe zu sehen
public class TraceFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        // „Davor" – wird ausgeführt, bevor der Request weiter in der Chain übergeben wird (inkl. Spring Security / MVC)
        System.out.println("Filter: before controller");

        // Schlüsselaufruf: Kontrolle an den nächsten Filter/Servlet übergeben
        chain.doFilter(request, response);

        // „Danach" – wird ausgeführt, wenn die Verarbeitung weiter unten in der Chain abgeschlossen ist
        System.out.println("Filter: after controller");
    }
}

Zwei wichtige Details. Erstens ist chain.doFilter(...) buchstäblich „gib die Kontrolle an den Nächsten weiter". Zweitens habe ich @Order(Ordered.HIGHEST_PRECEDENCE) hinzugefügt, damit der Filter so früh wie möglich ausgeführt wird. Das brauchen wir nicht für die Produktion, sondern damit du im Lernexperiment seine Ausgabe garantiert siehst, auch wenn andere Filter den Request stoppen wollen.

Und jetzt der „ehrlichste" Teil. Ein Filter kann den Request komplett blockieren. Das klingt hart, aber genau so soll Sicherheit funktionieren: Wenn es nicht erlaubt ist – dann ist es nicht erlaubt, und den Controller muss man damit nicht belästigen. Hier ist ein maximal anschauliches Beispiel für „Eingang zum Zimmer gesperrt":

package com.example.securecontent.common.web;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class BlockMeFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        // In der Servlet-API kommen Basis-Interfaces – wir casten auf HTTP-spezifische Typen
        HttpServletRequest req = (HttpServletRequest) request;

        // Lernlogik: Zugang zu einem bestimmten URI „am Eingang" sperren, noch vor MVC/Controller
        if ("/api/me".equals(req.getRequestURI())) {
            HttpServletResponse resp = (HttpServletResponse) response;
            resp.setStatus(403); // 403 Forbidden zurückgeben und den Request NICHT weiter in der Chain leiten
            return; // Wichtig: ohne return wird die Chain fortgesetzt
        }

        // Wenn wir nicht blockieren – normale Verarbeitung fortsetzen
        chain.doFilter(request, response);
    }
}

Dieser Filter ist eine Lern-„Parodie" auf die Idee von Security. Im echten Leben würde man das für die Autorisierung nicht so machen (dafür haben wir Spring Security), aber als Demonstration, dass der Controller überhaupt nicht aufgerufen werden muss, funktioniert er perfekt.

3. DispatcherServlet: Einstiegspunkt in Spring MVC

Der DispatcherServlet ist das zentrale Servlet von Spring MVC, das das Muster „Front Controller" implementiert. Das heißt, der Container übergibt den Request nicht direkt an den Controller, sondern an den DispatcherServlet, und dieser entscheidet innerhalb von Spring MVC, welcher Controller zu diesem Request passt, wie Path-Variablen gebunden werden, wie der Body geparst wird, wie Konverter angewendet werden, wie die Methode aufgerufen wird und wie die Antwort gebildet wird.

Wenn man es auf das Niveau „damit der Kopf nicht explodiert" vereinfacht, funktioniert der DispatcherServlet nach dem Schema: „Request empfangen – Handler gefunden – Handler aufgerufen – Ergebnis in Antwort umgewandelt". Und für unsere Lektion ist hier das Wichtigste: Der DispatcherServlet lebt nach den Filtern. Alles, was wir im vorherigen Abschnitt besprochen haben, kann noch vor dem MVC passieren.

Um das selbst zu spüren, reicht ein einfacher Controller. Zum Beispiel unser Endpoint der persönlichen Zone:

package com.example.securecontent.profile;

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

@RestController
public class MeController {

    @GetMapping("/api/me") // Endpoint der „persönlichen Zone": gut geeignet, um zu beobachten, ob Security uns blockiert
    public String me() {
        // Marker: wenn diese Zeile nicht ausgegeben wird, hat der Request den Controller nicht erreicht
        System.out.println("Controller method called");
        return "me"; // Lernantwort
    }
}

Wenn du einen Request machst und diese Zeile nicht erscheint, bedeutet das nicht, dass dein „Controller kaputt ist". Es bedeutet, dass der Request ihn nicht erreicht hat. Die Gründe können unterschiedlich sein, aber in einem Security-Kurs verdächtigen wir zuerst die Filter und die Security-Chain.

Von nun an ist es nützlich, sich eine Gewohnheit anzueignen: Wenn du „seltsames Verhalten eines Endpoints" siehst, frag dich zuerst: „Hat der Request überhaupt den DispatcherServlet erreicht?" Falls nicht – schau dir die Filter und Security an.

4. Spring Security in der Filter-Chain

Jetzt setzen wir alles zu einem Bild zusammen. Spring Security in einer Servlet-Anwendung ist kein einzelnes „magisches if", sondern eine Infrastruktur auf Filter-Ebene. Der Request durchläuft zunächst die Servlet-Filter-Chain, und irgendwo darin steht der Security-Teil, der: (a) den Request weiterlassen, (b) die Verarbeitung mit Daten anreichern kann, damit die Anwendung den Zustand des Requests versteht, oder (c) den Request stoppen und eine Antwort an den Client zurückgeben kann.

Das Wichtigste in dieser Lektion ist, sich nicht die Klassennamen zu merken (davon werden es noch viele sein), sondern den Platz im Request-Lifecycle. Wenn die Security-Entscheidung vor dem DispatcherServlet getroffen wird, kann dein Controller nicht ausgeführt werden, deine Services werden nicht aufgerufen, und das Repository sieht den Request nicht. Für dich sieht das manchmal aus wie „Ich habe doch eine Methode geschrieben, warum funktioniert sie nicht?", aber in Wirklichkeit „sollte sie gar nicht gestartet werden".

Lass uns ein möglichst einfaches Schema zeichnen. Es ist ein Lernschema, aber sehr „praxisnah" – damit das Modell „URL → Methode" in deinem Kopf aufhört zu leben.

flowchart TD
    Client["Client: Browser / Postman / curl"]
    Container["Servlet-Container"]
    Filters["Servlet-Filter"]
    Security["Spring Security (als Teil der Filter-Chain)"]
    DS["DispatcherServlet (Spring MVC)"]
    Controller["Controller-Methode"]
    Response["HTTP-Response"]

    Client --> Container --> Filters --> Security --> DS --> Controller --> Response

Jetzt binden wir das an unser Projekt Secure Content Platform API. Wir haben grob zwei Zonen:

- /api/public/articles – öffentliche Zone (nach Geschäftslogik);
- /api/me – persönliche Zone, zu der anonyme Benutzer definitiv keinen Zugang haben dürfen.

Auf Code-Ebene können sie sich gleich einfach anfühlen – zwei Methoden. Aber im Request-Lifecycle gibt es bereits einen wesentlichen Unterschied: Security stoppt /api/me früher, während es /api/public/articles durchlässt. Wir haben diesen Effekt bereits in der Praxis beobachtet: starter-security hat „alles geschlossen", und du hast andere Antworten erhalten, noch ohne eigene Konfiguration.

Und hier ist die wichtigste Schlussfolgerung: Spring Security ist ein Mechanismus, der „am Eingang" steht, vor dem MVC, und deshalb kann er das Schicksal eines Requests beeinflussen, bevor dein Code ihn überhaupt sieht.

Aber nur zu sagen „Security lebt in Filtern" reicht nicht. Wenn der Container den Request als Erstes sieht, stellt sich die natürliche Frage: Wie übergibt diese Servlet-Welt überhaupt die Kontrolle an die Spring-Beans der Sicherheit?

5. Mini-Experiment: der Weg eines Requests

Jetzt folgt ein kleines Experiment, damit du nicht nur die Theorie gelesen hast, sondern die Ursache-Wirkungs-Beziehung selbst siehst. Wir werden in dieser Lektion keine Security-Konfiguration schreiben (das kommt später), aber wir können die Pipeline selbst „beleuchten": Der Filter schreibt Nachrichten, der Controller schreibt Nachrichten, und du vergleichst, was tatsächlich passiert ist.

Wenn du TraceFilter und MeController hinzugefügt hast, ergeben sich drei mögliche Szenarien:

Szenario A: Der Request hat den Controller nicht erreicht wegen Security (z.B. du hast dich nicht authentifiziert). Dann siehst du wahrscheinlich nur die Filter-Ausgabe „before", aber kein „Controller method called". Manchmal siehst du noch „after" (wenn die Chain die Kontrolle zurückgegeben hat), aber den Controller selbst nicht.

Szenario B: Der Request hat den Controller erreicht (z.B. du hast den Request mit den richtigen Zugangsdaten für den Default-User aus Tag 2 gemacht). Dann siehst du:

  • „Filter: before controller"
  • „Controller method called"
  • „Filter: after controller"

Das ist der Beweis, dass der Controller tatsächlich in der Mitte der Chain steht und nicht „als Erster".

Szenario C: Der Request hat den Controller nicht erreicht, weil er vom „eigenen" Filter gestoppt wurde (z.B. BlockMeFilter). Dann siehst du ebenfalls, dass der Controller schweigt, aber der Grund liegt nicht in Spring Security, sondern in deinem Lernblocker. Das ist nützlich: Du beginnst zu unterscheiden zwischen „MVC nicht erreicht" und „erreicht, aber im Controller abgebrochen".

Um die Erwartungen leichter abgleichen zu können, hier die typische Ausgabe-Logik (Pseudo-Beispiel; keine Byte-Garantie, aber inhaltlich so):

Filter: before controller
Filter: after controller

Wenn der Controller nicht aufgerufen wurde, fehlt die Zeile „Controller method called". Und das ist bereits eine wertvolle Information.

6. Typische Fehler bei der Arbeit mit der Filter-Chain

Fehler Nr. 1: Die Ursache einer Ablehnung nur im Controller suchen.
Wenn du 401, 403, eine Weiterleitung zur Login-Seite oder „in Postman verhält es sich anders" siehst, ist es sehr verlockend, @GetMapping, DTOs und Services zu bearbeiten. Aber wenn der Request den DispatcherServlet nicht erreicht hat, sind all diese Änderungen so, als würde man die Türklinke in einem Zimmer reparieren, in das einen die Sicherheit nicht reingelassen hat.

Fehler Nr. 2: Denken, dass ein Filter nur „Logging für Fortgeschrittene" ist.
In der Praxis ist ein Filter ein Punkt, der das Schicksal eines Requests komplett verändern kann. Er kann die Verarbeitung stoppen, einen Status setzen, Header hinzufügen, einen Redirect machen, den Request nicht ins MVC lassen. Und Spring Security funktioniert genau so: Es „rät dem Controller" nicht, es handelt früher.

Fehler Nr. 3: DispatcherServlet und Controller verwechseln.
Der Controller ist dein Code mit @RestController. Der DispatcherServlet ist der infrastrukturelle Einstiegspunkt ins MVC. Dazwischen steckt viel Mechanik. Wenn du sie nicht unterscheidest, fängst du unweigerlich an zu denken „Spring MVC = Controller", was wieder zum falschen Modell „URL → Methode" zurückführt.

Fehler Nr. 4: Versuchen, die gesamte Sicherheit mit Annotationen an Endpoints zu erklären.
Annotationen am Controller sind wichtig, aber sie beantworten nicht die Frage „Wann wird diese Methode überhaupt aufgerufen?". Wenn Security den Request in den Filtern gestoppt hat, hilft keine Annotation „innerhalb des MVC" – weil das MVC einfach nicht dorthin gelangt ist.

1
Aufgabe
Spring Security, Level 3, Lektion 0
Gesperrt
Filter um den Controller
Filter um den Controller
1
Aufgabe
Spring Security, Level 3, Lektion 0
Gesperrt
Stoppen der Anfrage im Filter vor MVC
Stoppen der Anfrage im Filter vor MVC
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION