1. Jawny SecurityFilterChain
Czas przejść od obserwacji do zarządzania. Zamiast żyć według zasady „jak Spring Security postanowił", zaczniemy jawnie opisywać reguły dostępu w jednym miejscu – we własnym SecurityFilterChain. I zaczniemy maksymalnie ostrożnie: najpierw szkielet, bez próby skonfigurowania całego świata jednego wieczoru (jak to często bywa w piątki).
„Domyślnie" – to nie strategia
Szczerze mówiąc, „zachowanie domyślne" to jak mieszkanie w mieszkaniu, gdzie pilot od domofonu ma sąsiad, a ty po prostu masz nadzieję, że jest normalnym człowiekiem. Dopóki potrzebujesz tylko „żeby działało", jeszcze można tak żyć. Ale gdy tylko projekt zyska własną mapę dostępu – publiczne artykuły, strefa osobista, redaktorska, adminowska – poleganie na domyślnych staje się niebezpieczne: nie kontrolujesz, co dokładnie „zostało otwarte", co „zamknięte" i dlaczego.
Z domyślnymi Spring Security jest jeszcze inny problem: są wygodne dokładnie do momentu, gdy próbujesz wytłumaczyć koledze (albo sobie za dwa tygodnie), dlaczego konkretny endpoint odpowiada 401, 403 albo w ogóle przekierowuje na stronę logowania w HTML. I tu wychodzi na jaw nieprzyjemna rzecz: gdy konfiguracja jest niejawna, nie ma „jednej strony prawdy", w którą można wbić palcem i powiedzieć: „Patrz, tu są reguły".
Powiedzmy wprost: w prawdziwej aplikacji backendowej bezpieczeństwo nie bywa „samo z siebie". Jest albo zaprojektowane i wyrażone w kodzie, albo to „zestaw przypadkowości, które na razie się nie posypały". Naszym celem jest przeniesienie projektu z drugiej kategorii do pierwszej, bez zamieniania Spring Security w religię i bez poświęcania czytelności.
Żeby ta myśl utrwaliła się, wygodnie jest porównać dwa podejścia – jak zwykle robią to początkujący i jak będziemy robić w kursie:
| Podejście | Gdzie żyją reguły dostępu | Co zazwyczaj dzieje się z czasem |
|---|---|---|
| „Sprawdzę w kontrolerze" | if (user == null) throw ... w @RestController | Reguły się rozłażą, duplikują, nie sposób ich zobaczyć w całości, pojawiają się dziury |
| „Niech Spring Security sam" | W auto-config i domyślnych | Niejasne, dlaczego tak działa, trudno debugować, trudno rozszerzać pod projekt |
| Jawny SecurityFilterChain | W jednym miejscu, w konfiguracji security | Reguły czyta się od góry do dołu, łatwiej sprawdzać i zmieniać, mniej niespodzianek |
Idziemy właśnie trzecią drogą: nie „ręcznie w kontrolerach" i nie „ufamy domyślnym", lecz robimy jawną konfigurację jako normalny artefakt inżynierski.
2. SecurityFilterChain w głowie i w Spring
Słowo SecurityFilterChain brzmi jak coś z powieści o spisku, ale w praktyce jest prostsze. Model mentalny jest taki: SecurityFilterChain to zestaw reguł i filtrów, przez które przechodzi żądanie zanim trafi do twoich kontrolerów. Jeśli łańcuch zdecyduje, że żądania nie można przepuścić dalej, kontroler nawet nie dowie się, że ktoś tam pukał.
Ważne jest, żeby nie pomylić dwóch poziomów. W trzecim dniu mówiliśmy, że istnieje infrastruktura DelegatingFilterProxy → FilterChainProxy → SecurityFilterChain. Dziś patrzymy z drugiej strony: SecurityFilterChain to nie tylko „coś wewnątrz Spring Security", lecz to, co możemy zadeklarować jako bean, żeby powiedzieć: „Oto reguły naszej aplikacji, a nie abstrakcyjny default".
Dla utrwalenia oto mały obrazek (mocno uproszczony, ale przydatny):
flowchart TD
%% Uproszczona trasa żądania: od klienta przez filtry do kontrolerów
C[Klient] -->|HTTP| F[Servlet filters]
%% Most między Servlet API a Spring Security
F --> D[DelegatingFilterProxy]
D --> P[FilterChainProxy]
%% Tu podłączany jest nasz łańcuch (bean), a nie „magia domyślna"
P --> S["SecurityFilterChain (nasz bean)"]
S --> M[DispatcherServlet → Controllers]
Czyli łańcuch to dosłownie „kołowrót" przy wejściu. Dopóki kołowrót nie ma zrozumiałych reguł – albo są ukryte w auto-config – możesz ile chcesz pisać piękną logikę biznesową w serwisach: żądanie albo tam nie dotrze, albo dotrze nie tam, gdzie oczekiwałeś.
Jeszcze jedna myśl, która szczególnie pomaga początkującym: SecurityFilterChain to nie „jak sprawdzać hasło". Mechanika weryfikacji użytkownika (autentykacja) to osobna historia, i zapoznaliśmy się już z jej głównymi uczestnikami (AuthenticationManager, providerzy itp.). Dziś skupiamy się na czymś innym: jakie żądania w ogóle są dostępne dla kogo, czyli na bazowych regułach dostępu na poziomie URL. Ręczne sprawdzanie hasła w kontrolerze nie będzie potrzebne (i to dobra wiadomość).
3. HttpSecurity jako konstruktor reguł
Teraz zobaczmy, jak to wygląda w kodzie. W Spring Security zazwyczaj nie tworzymy SecurityFilterChain ręcznie „od zera". Zamiast tego Spring daje nam obiekt HttpSecurity – wygodnie postrzegać go jako builder, czyli konstruktor konfiguracji dla jednego łańcucha bezpieczeństwa.
HttpSecurity jest ważny z dwóch powodów. Po pierwsze, przez niego opisujemy, co nasze bezpieczeństwo ma robić: reguły dostępu, obsługę błędów, mechanizmy logowania i inne elementy. Po drugie, HttpSecurity to „szkic", który zamienia się w gotowy łańcuch dopiero w momencie wywołania build(). Dlatego drugi kluczowy termin dnia to http.build().
Ludzkim językiem ten proces wygląda tak: „Zebraliśmy ustawienia, a teraz «zapiekamy» je w gotowy obiekt SecurityFilterChain". Tak, to prawie jak pieczenie: przed build() masz ciasto, po nim – bułeczkę. Jeśli zapomnisz build(), bułeczki niestety nie będzie, a aplikacja albo nie wystartuje, albo będzie zachowywać się nie tak, jak oczekiwałeś.
Na tym etapie warto trzymać fokus: dziś nie próbujemy konfigurować wszystkiego. Potrzebujemy dwóch rzeczy: zrozumieć, że HttpSecurity to punkt składania, i zobaczyć minimalny szkielet SecurityFilterChain, który stanie się „domem" dla wszystkich kolejnych reguł tego dnia.
4. Szkielet w kodzie: @Bean SecurityFilterChain
Pora napisać ten szkielet. I tu jest przyjemna sprawa: kod jest naprawdę krótki. Potrzebna jest klasa konfiguracyjna i jedna metoda zwracająca SecurityFilterChain. Spring sam przekaże nam HttpSecurity, a my zwrócimy http.build().
Zacznijmy od maksymalnie „pustej" formy, żeby zobaczyć strukturę:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration // Konfiguracja Spring: stąd zostanie pobrany bean
public class SecurityConfig {
@Bean // Robimy SecurityFilterChain zarządzanym beanem, a nie "domyślnym z auto-config"
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// Na razie to "pusty szkielet": nic nie konfigurujemy, po prostu składamy łańcuch
return http.build();
}
}
Jeśli patrzysz teraz na to i myślisz: „I to wszystko?", to tak – co do formy to naprawdę wszystko. Ale sens jest inny: od tej chwili bezpieczeństwo aplikacji staje się twoim kodem, a nie tylko auto-config wewnątrz startera. Masz punkt kontrolny, do którego potem będą dodawane reguły: co jest publiczne, co wymaga logowania, co dostępne tylko dla redaktora lub administratora.
Właśnie tu dzieje się ważna zmiana w porównaniu z domyślnym starterem. Dopóki nie było własnego SecurityFilterChain, politykę dostępu na poziomie żądań wyznaczała standardowa konfiguracja Spring Security. Gdy tylko łańcuch jest zadeklarowany jako bean, reguły „kogo gdzie wpuszczać" są już czytane z naszego kodu. Przy tym uczelnicze podpory jak form login, HTTP Basic czy generated user mogą na razie zostać na miejscu – ale teraz działają wewnątrz polityki, którą sami złożyliśmy jawnie.
Jest tu jednak praktyczny niuans. Czysty http.build() to tylko szkielet. W prawdziwej aplikacji, żeby zachowanie było przewidywalne i nie „pływało" od wersji do wersji, zazwyczaj ustawiamy przynajmniej minimalny baseline. W ramach kursu wygodnie nam zachować ideę secure-by-default: najpierw wszystko zamknięte, potem punktowo otwieramy publiczne strefy.
Poniżej – ten roboczy baseline, którego wygodnie trzymać się dalej: szkielet łańcucha i bazowe sposoby logowania pozostają na miejscu, a główna praca potem odbywa się już w samych regułach dostępu.
Dlatego oto „bezpieczny bazowy baseline": aplikacja pozostaje zamknięta, ale mamy już mechanizm autentykacji, żeby w ogóle było czym sprawdzać dostęp:
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 {
return http
// Bazowa reguła: każde żądanie wymaga autentykacji
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
// Włączamy form login "domyślnie", żeby można było testować przez przeglądarkę
.formLogin(Customizer.withDefaults())
// Włączamy HTTP Basic "domyślnie", żeby wygodnie sprawdzać przez Postman/curl
.httpBasic(Customizer.withDefaults())
// Finalizujemy builder: bez build() łańcuch nie zostanie stworzony
.build();
}
}
Tu ważne jest prawidłowe odczytanie tego, co się dzieje, bez przedwczesnej teorii. Nie studiujemy teraz authorizeHttpRequests i nie analizujemy, czym authenticated() różni się od hasRole() – to tematy kolejnych lektur. Teraz tylko utrwalamy sens: mamy miejsce, gdzie będą żyć reguły, i minimalną możliwość logowania, żeby ręcznie sprawdzić dostęp.
Jeśli martwisz się o formLogin(...) i httpBasic(...), to dobra wiadomość: nie będziemy ich dostosowywać i nie robimy z nich osobnego tematu. Po prostu zostawiamy włączone bazowe mechanizmy, które zazwyczaj są przydatne na starcie do testowania dostępu zarówno przeglądarką, jak i Postmanem. Szczegółowo o form login i HTTP Basic porozmawiamy we właściwym czasie – teraz nie zagłębiamy się tam.
I jeszcze jedna ważna rzecz: w Spring Boot zazwyczaj nie trzeba ręcznie rejestrować filtrów. Wystarczy, żeby SecurityFilterChain był zadeklarowany jako @Bean. Boot i Spring Security go podchwycą, a FilterChainProxy zacznie używać właśnie tego łańcucha.
5. Położenie konfiguracji i jeden łańcuch
Gdy początkujący po raz pierwszy dodaje konfigurację bezpieczeństwa, często ma ochotę położyć ją gdziekolwiek: w config, w common, w „no to wrzucę wprost do klasy main, żeby nie szukać". Ale security to ta warstwa, którą potem czyta się częściej, niż by się chciało. Dlatego lepiej od razu dać jej zrozumiały adres.
W naszym projekcie Secure Content Platform API trzymamy kod związany z bezpieczeństwem w osobnej strefie, na przykład tak:
src/main/java/com/example/securecontent/security/config/SecurityConfig.java
Dlaczego tak jest wygodniej? Po pierwsze, wszystko związane z bezpieczeństwem jest razem i nie miesza się z biznesowymi feature'ami content, profile, admin. Po drugie, gdy szukasz, dlaczego /api/admin/users nagle stał się publiczny, nie przeglądasz dwudziestu kontrolerów – otwierasz jedną konfigurację.
Jeszcze od razu uzgodnijmy ważne ograniczenie tego dnia: dziś robimy jeden łańcuch. Spring Security potrafi pracować z kilkoma SecurityFilterChain, i niekiedy jest to mocne i przydatne, ale w fundamentalnym kursie łatwo to zamienia się w „dlaczego moje żądanie trafiło do innego chain?". Dlatego w ramach Checkpoint 1 zadanie jest prostsze: jeden łańcuch, jedna czytelna lista reguł, od góry do dołu.
6. Typowe błędy przy pracy z SecurityFilterChain
Błąd nr 1: zapomniano @Bean lub @Configuration, i „skonfigurowałeś security", ale Spring o tym nie wie.
Zdarza się to ciągle: klasa ładna, metoda prawidłowa, ale zapomniano adnotacji – i aplikacja dalej żyje na auto-config. Leczy się prosto: sprawdź, czy metoda jest oznaczona @Bean, a klasa – @Configuration (lub że w ogóle jest importowana i skanowana przez Spring).
Błąd nr 2: zwrócono SecurityFilterChain, ale nie wywołano build().
HttpSecurity to nie SecurityFilterChain. To builder. Jeśli zwracasz nie to lub zapominasz sfinalizować konfigurację, w istocie zostawiasz „niedogotowany" obiekt. Finał musi być jeden i wyraźny: return http.build() lub łańcuch return http....build().
Błąd nr 3: próba „skonfigurować wszystko naraz", jakby to był konkurs na najdłuższy DSL.
U początkujących często pojawia się impuls: „Skoro już otworzyłem HttpSecurity, dodam od razu CORS, CSRF, session policy, handlery, custom filtry, obsługę wyjątków…". Wynik – konfiguracja na 200 wierszy, której nikt nie rozumie. W tym module nasz cel jest inny: krótka i czytelna konfiguracja. Resztę będziemy dodawać stopniowo, gdy pojawi się kontekst.
Błąd nr 4: przenoszenie logiki security do kontrolerów „bo na razie tak szybciej".
Jeśli już dodałeś SecurityFilterChain, ale nadal piszesz w kontrolerach if (!isAdmin) return 403, dostajesz podwójny system bezpieczeństwa, który z czasem zaczyna sam sobie przeczyć. Reguły URL powinny żyć w konfiguracji, a nie być rozmazane po endpointach. Inaczej Spring Security zamienia się w dekorację, a nie architektoniczny warstę.
Błąd nr 5: próba „wyłączenia wszystkiego, co przeszkadza", żeby „po prostu działało".
Na pierwszych krokach szczególnie duża jest pokusa napisania czegoś w stylu „wyłączę ochronę, potem włączę z powrotem". Problem polega na tym, że to „potem" zazwyczaj nie nadchodzi, a nawyk się utrwala. W tym kursie trzymamy w głowie secure-by-default: najpierw zamknięte, potem świadomie otwieramy potrzebne strefy. Trochę nudniejsze, ale znacznie bezpieczniejsze i przewidywalniejsze.
GO TO FULL VERSION