CodeGym /Kursy /Spring Security /Spring Security w request lifecycle

Spring Security w request lifecycle

Spring Security
Poziom 3, Lekcja 0
Dostępny

1. Model endpoint = kontroler

URL nie oznacza jeszcze, że żądanie dotarło do metody kontrolera. Prawie każdy początkujący – i, powiedzmy uczciwie, wiele doświadczonych osób – na początku myśli tak: jest URL, znaczy jest metoda kontrolera, znaczy żądanie „trafiło do metody" i tam „coś się wydarzyło". Jest to wygodne i zrozumiałe, ale na prawdziwym serwerze między „przyszło żądanie HTTP" a „wykonała się metoda kontrolera" jest cały korytarz infrastruktury. I właśnie w tym korytarzu mieszka bezpieczeństwo.

Wyobraź sobie, że kontroler to gabinet lekarski. Pacjent (żądanie HTTP) nie teleportuje się od razu do lekarza. Najpierw wchodzi do budynku (do aplikacji), przechodzi przez rejestrację, ochronę, ewentualnie szatnię (filtry) – i dopiero wtedy trafia do gabinetu (kontrolera). Jeśli ochrona zdecyduje, że wejść nie można (brak przepustki, zła pora, zła rola), gabinet lekarza nawet nie dowie się, że ktoś przychodził. To nie jest „błąd gabinetu". To prawidłowo zorganizowane wejście do budynku.

Właśnie dlatego w kursie o Spring Security tak uparcie powtarzamy: „nie szukaj problemu tylko w kontrolerze". Bardzo często kontroler po prostu nie jest wywoływany, bo decyzja została podjęta wcześniej.

Servlet container: pierwszy w kolejce

Słowa servlet container brzmią jak coś z powieści Lovecrafta, ale w rzeczywistości to zupełnie przyziemna rzecz. Servlet container to środowisko, które przyjmuje przychodzące żądania HTTP i uruchamia ich przetwarzanie zgodnie z regułami Servlet API. W aplikacji Spring Boot jest to zazwyczaj wbudowany serwer (np. Tomcat), który startuje razem z aplikacją i zaczyna nasłuchiwać na porcie.

Gdy uruchamiasz Spring Boot, dzieje się ważna „magia" (w rzeczywistości – inżynieria): podnosi się kontener, rejestrują się servlety i filtry, tworzony jest Spring ApplicationContext, a kontener uczy się wywoływać Springową część aplikacji. Ale główne do zapamiętania jest to: żądanie jako pierwsze widzi kontener, a nie Spring MVC i nie twój kontroler.

Upraszczając, przetwarzanie żądania wygląda tak:

1) żądanie dotarło do kontenera;
2) kontener przepuścił żądanie przez łańcuch servlet filters;
3) kontener przekazał żądanie do jednego z servletów (w naszym przypadku – do DispatcherServlet Spring MVC);
4) Spring MVC znalazł kontroler i wywołał metodę;
5) odpowiedź ruszyła tą samą drogą z powrotem.

I właśnie w punkcie nr 2 – w „łańcuchu filtrów" – mieszka znaczna część Spring Security. Dlatego pytanie „gdzie jest Spring Security?" to nie pytanie filozoficzne, lecz całkiem praktyczne: w którym miejscu przetwarzania żądania stoi.

2. Servlet filters: ramka wokół żądania

Filtr w Servlet API to element taśmy produkcyjnej, który może wykonać kod przed tym, jak żądanie trafi do servletu, i po tym, jak servlet skończy pracę. Ten efekt „opakowania" jest bardzo ważny: filtr może nie tylko logować, ale i modyfikować żądanie/odpowiedź, a niekiedy – w ogóle zakończyć przetwarzanie zanim ktokolwiek dotrze do MVC.

Filtr wygodnie wyobrażać sobie jako „ramkę", która owija wszystko, co idzie dalej:

- „przed" – przygotowanie, sprawdzenia, logowanie;
- chain.doFilter(...) – przepuszczenie dalej;
- „po" – zakończenie, metryki, końcowe nagłówki, audyt.

Minimalny filtr edukacyjny (w naszym projekcie można go tymczasowo umieścić np. w com.example.securecontent.common.web) wyglądałby tak:

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) // Polecenie: wykonaj filtr jak najwcześniej, żeby zobaczyć go w wyjściu
public class TraceFilter implements Filter {

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

        // "Przed" – wykonuje się przed przekazaniem żądania dalej w łańcuchu (w tym Spring Security / MVC)
        System.out.println("Filter: before controller");

        // Kluczowe wywołanie: przekazujemy sterowanie do następnego filtra/servletu
        chain.doFilter(request, response);

        // "Po" – wykonuje się, gdy przetwarzanie żądania niżej w łańcuchu się zakończyło
        System.out.println("Filter: after controller");
    }
}

Dwa ważne szczegóły. Po pierwsze, chain.doFilter(...) to dosłownie „przekaż sterowanie dalej". Po drugie, dodałem @Order(Ordered.HIGHEST_PRECEDENCE), żeby filtr wykonywał się jak najwcześniej. Potrzebujemy tego nie na „produkcję", lecz żeby w edukacyjnym eksperymencie mieć gwarancję zobaczenia jego wyjścia, nawet jeśli inne filtry zechcą zatrzymać żądanie.

A teraz najbardziej „uczciwa" część. Filtr może w ogóle nie przepuścić żądania dalej. Brzmi surowo, ale właśnie tak powinno działać bezpieczeństwo: jeśli nie wolno – to nie wolno, i nie ma po co niepokoić kontrolera. Oto maksymalnie czytelny przykład „zablokowano wejście przed gabinetem":

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 {

        // W Servlet API przychodzą bazowe interfejsy – rzutujemy na typy specyficzne dla HTTP
        HttpServletRequest req = (HttpServletRequest) request;

        // Logika edukacyjna: blokujemy dostęp do konkretnego URI "przy wejściu", jeszcze przed MVC/kontrolerem
        if ("/api/me".equals(req.getRequestURI())) {
            HttpServletResponse resp = (HttpServletResponse) response;
            resp.setStatus(403); // Zwracamy 403 Forbidden i NIE przepuszczamy żądania dalej w łańcuchu
            return; // Ważne: bez return łańcuch by się kontynuował
        }

        // Jeśli nie blokujemy – kontynuujemy normalne przetwarzanie
        chain.doFilter(request, response);
    }
}

Ten filtr to edukacyjna „parodia" idei security. W prawdziwym życiu nie będziesz tak robił dla autoryzacji (do tego mamy Spring Security), ale jako demonstracja, że kontroler może w ogóle nie zostać wywołany, działa idealnie.

3. DispatcherServlet: wejście do Spring MVC

DispatcherServlet to centralny servlet Spring MVC, który implementuje wzorzec „Front Controller". Czyli kontener nie przekazuje żądania bezpośrednio do kontrolera, lecz do DispatcherServlet, a ten już wewnątrz Spring MVC decyduje, który kontroler pasuje do tego żądania, jak powiązać path variables, jak sparsować ciało, jak zastosować konwertery, jak wywołać metodę i jak zbudować odpowiedź.

Upraszczając do poziomu „żeby nie wysadzić mózgu", DispatcherServlet działa według schematu: „złapał żądanie – znalazł handler – wywołał handler – zamienił wynik w odpowiedź". I dla naszej lektury najważniejsze jest jedno: DispatcherServlet żyje po filtrach. Wszystko, co omawialiśmy w poprzedniej sekcji, może się wydarzyć jeszcze przed MVC.

Żeby to poczuć rękami, wystarczy mieć prosty kontroler. Na przykład nasz endpoint strefy osobistej:

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 "strefy osobistej": na nim wygodnie obserwować, czy security nas blokuje
    public String me() {
        // Znacznik: jeśli ten string nie jest drukowany, żądanie nie dotarło do kontrolera
        System.out.println("Controller method called");
        return "me"; // Odpowiedź edukacyjna
    }
}

Jeśli robisz żądanie i ten wiersz nie jest wyświetlany, nie oznacza to, że „zepsuł się kontroler". Oznacza to, że żądanie do niego nie dotarło. Przyczyny bywają różne, ale w kursie security prawie zawsze najpierw podejrzewamy filtry i łańcuch bezpieczeństwa.

Od tej chwili przydatne jest wyrobienie nawyku: gdy widzisz „dziwne zachowanie endpointu", najpierw zapytaj siebie: „Czy żądanie w ogóle dotarło do DispatcherServlet?" Jeśli nie – patrzymy w filtry i security.

4. Spring Security w filter chain

Teraz zbieramy wszystko w jeden obraz. Spring Security w aplikacji servletowej to nie jeden „magiczny if", lecz infrastruktura na poziomie filtrów. Żądanie najpierw przechodzi przez servlet filter chain, a gdzieś wewnątrz niej stoi część security, która może: (a) przepuścić żądanie dalej, (b) uzupełnić przetwarzanie danymi, żeby aplikacja rozumiała, w jakim stanie jest żądanie, lub (c) zatrzymać żądanie i zwrócić odpowiedź klientowi.

Najważniejsze w tej lekturze – zapamiętać nie nazwy klas (będzie ich wiele), lecz miejsce w cyklu życia żądania. Jeśli decyzja security jest podjęta przed DispatcherServlet, twój kontroler może się nie wykonać, twoje serwisy nie zostaną wywołane, a repozytorium nie zobaczy żądania. Dla ciebie wygląda to niekiedy jak „przecież napisałem metodę, czemu nie działa?", ale w rzeczywistości „nie miała się uruchamiać".

Narysujmy maksymalnie prosty schemat. Jest edukacyjny, ale bardzo „praktyczny" – żeby w głowie przestał żyć model „URL → metoda".

flowchart TD
    Client["Klient: przeglądarka / Postman / curl"]
    Container["Servlet container"]
    Filters["Servlet filters"]
    Security["Spring Security (jako część filter chain)"]
    DS["DispatcherServlet (Spring MVC)"]
    Controller["Controller method"]
    Response["HTTP response"]

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

Teraz powiążmy to z naszym projektem Secure Content Platform API. Mamy warunkowo dwie strefy:

- /api/public/articles – strefa publiczna (z sensu biznesowego);
- /api/me – strefa osobista, gdzie anonimowym użytkownikom na pewno nie wolno.

Na poziomie kodu kontrolera mogą wyglądać jednakowo prosto – dwie metody. Ale w cyklu życia żądania różnica jest już istotna: security zatrzyma /api/me wcześniej, a /api/public/articles przepuści. Widzieliśmy już ten efekt w praktyce: starter-security „zamknął wszystko" i dostałeś inne odpowiedzi nawet bez własnej konfiguracji.

I oto główny wniosek: Spring Security to mechanizm stojący „na wejściu", przed MVC, więc potrafi zmienić los żądania zanim twój kod w ogóle je zobaczy.

Ale powiedzenie tylko „security żyje w filtrach" to za mało. Jeśli żądanie jako pierwsze widzi kontener, pojawia się naturalne pytanie: jak ten servlet-świat w ogóle przekazuje sterowanie do Springowych beanów bezpieczeństwa?

5. Mini-eksperyment: trasa żądania

Teraz będzie mały eksperyment, żebyś nie tylko przeczytał teorię, lecz zobaczył związek przyczynowo-skutkowy. Nie będziemy w tej lekturze pisać konfiguracji security (to będzie później), ale możemy sami „podświetlić" pipeline: filtr pisze komunikaty, kontroler pisze komunikaty, a ty porównujesz, co się naprawdę wydarzyło.

Jeśli dodałeś TraceFilter i MeController, masz trzy możliwe scenariusze:

Scenariusz A: żądanie nie dotarło do kontrolera z powodu security (np. nie uwierzytelniłeś się). Wtedy prawdopodobnie zobaczysz tylko wyjście filtra „before", a "Controller method called" – nie. Niekiedy zobaczysz jeszcze "after" (jeśli łańcuch zwrócił sterowanie z powrotem), ale samego kontrolera nie będzie.

Scenariusz B: żądanie dotarło do kontrolera (np. wysłałeś żądanie z prawidłowymi danymi uwierzytelniającymi dla default user z 2. dnia). Wtedy zobaczysz:

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

To będzie dowód, że kontroler naprawdę jest w środku łańcucha, a nie stoi „jako pierwszy".

Scenariusz C: żądanie nie dotarło do kontrolera, bo zatrzymał je „własny" filtr (np. BlockMeFilter). Wtedy też zobaczysz, że kontroler milczy, ale przyczyną nie jest Spring Security, lecz twój edukacyjny bloker. Jest to przydatne: zaczynasz odróżniać „nie dotarło do MVC" od „dotarło, ale padło w kontrolerze".

Żeby łatwiej porównywać oczekiwania, oto typowa logika wyjścia (pseudo-przykład; nie gwarancja co do bajtu, ale sensuznie tak):

Filter: before controller
Filter: after controller

Jeśli kontroler nie został wywołany, wiersza "Controller method called" nie ma. I to już cenna informacja.

6. Typowe błędy przy pracy z filter chain

Błąd nr 1: szukać przyczyny odmowy tylko w kontrolerze.
Gdy widzisz 401, 403, przekierowanie na logowanie lub „w Postmanie działa inaczej", bardzo łatwo zacząć poprawiać @GetMapping, DTO i serwisy. Ale jeśli żądanie nie dotarło do DispatcherServlet, wszystkie te poprawki są jak naprawianie klamki drzwiowej w pokoju, do którego nie wpuścili cię ochroniarze.

Błąd nr 2: myśleć, że filtr to tylko „logowanie dla zaawansowanych".
W praktyce filtr to punkt, który może całkowicie zmienić los żądania. Może zatrzymać przetwarzanie, ustawić status, dodać nagłówki, zrobić przekierowanie, nie przepuścić żądania do MVC. I Spring Security działa właśnie tak: nie „doradza kontrolerowi", lecz działa wcześniej.

Błąd nr 3: mylić DispatcherServlet z kontrolerem.
Kontroler to twój kod z @RestController. DispatcherServlet to infrastrukturalne wejście do MVC. Między nimi jest dużo mechaniki. Jeśli ich nie rozróżniasz, nieuchronnie zaczniesz myśleć „Spring MVC = kontroler", co znowu wraca do fałszywego modelu „URL → metoda".

Błąd nr 4: próbować wyjaśnić całe bezpieczeństwo adnotacjami na endpointach.
Adnotacje na kontrolerze są ważne, ale nie odpowiadają na pytanie „kiedy w ogóle ta metoda zostanie wywołana". Jeśli security zatrzymało żądanie w filtrach, żadna adnotacja „wewnątrz MVC" nie pomoże – bo MVC do niej po prostu nie dotarł.

1
Zadanie
Spring Security, poziom 3, lekcja 0
Niedostępne
Filtr wokół kontrolera
Filtr wokół kontrolera
1
Zadanie
Spring Security, poziom 3, lekcja 0
Niedostępne
Zatrzymanie zapytania w filtrze przed MVC
Zatrzymanie zapytania w filtrze przed MVC
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION