1. Rola anonymous user: model bez pustki
Spring Security stara się, żeby każde żądanie można było opisać jednakowo. Bardzo chce się myśleć po prostu: „jeśli użytkownik zalogował się – jest, jeśli nie zalogował – go nie ma". W programowaniu taka prostota zazwyczaj kończy się tym, że połowa kodu zamienia się w sprawdzenia pod kątem null, a druga połowa w nieoczekiwane bugi, bo gdzieś zapomniano jednego sprawdzenia. Spring Security rozwiązuje ten problem nie filozofią, lecz inżyniersko.
Wyobraź sobie nasz projekt Secure Content Platform API. Zgodnie z założeniami domeny mamy publiczne artykuły (/api/public/articles) i strefę osobistą (/api/me, szkice, profil). Nawet jeśli jeszcze nie napisałeś ani jednego własnego ustawienia bezpieczeństwa, widziałeś już w drugim dniu, że Spring Security potrafi „odsiewać" żądania przed kontrolerem. I właśnie tu pojawia się ważna myśl: warstwa security musi umieć jednakowo prawidłowo opisywać zarówno żądanie od zalogowanego użytkownika, jak i żądanie „gościa".
Jeśli „gość" to po prostu brak czegokolwiek, w stylu „w kontekście niczego nie ma", dalej bardzo trudno jest budować system reguł. Ale jeśli „gość" to zrozumiały techniczny stan, reguły stają się prostsze: „wszystkie żądania przechodzą przez ten sam mechanizm, tylko w kontekście leżą różne dane".
W Spring Security ten stan nosi nazwę anonymous authentication: zamiast idei „niczego nie ma" pojawia się idea „jest użytkownik anonimowy".
2. SecurityContext: stan żądania
W tej sekcji ważne jest, żeby nie próbować od razu zapamiętać wszystkich klas. Lepiej wyobrazić sobie jedną prostą rzecz: podczas przetwarzania żądania HTTP aplikacja potrzebuje miejsca, gdzie przechowywana jest odpowiedź na pytanie „kto teraz wysyła żądanie". Właśnie tym miejscem jest SecurityContext.
SecurityContext to mały obiekt-kontener, który przechowuje (przede wszystkim) obiekt Authentication. Można myśleć o nim jak o identyfikatorze na konferencji. Gdy idziesz korytarzem – przez filtry i MVC – identyfikator jest przy tobie. Po identyfikatorze można zrozumieć: jesteś gościem, uczestnikiem, organizatorem, prelegentem – czyli jaki masz status. I co ważne: nawet jeśli „po prostu przyszedłeś popatrzeć", identyfikator i tak jest przydatny – ochrona nie musi zgadywać, kim jesteś.
Z punktu widzenia Spring Security, SecurityContext jest tworzony i przygotowywany wewnątrz security filter chain na wczesnych etapach, żeby reszta łańcucha mogła na nim polegać. To czyni model stabilnym: kolejne filtry, MVC i twój kod nie muszą wymyślać „jeszcze jednego sposobu na rozpoznanie użytkownika".
Upraszczając do formuły, dla każdego żądania idea jest taka:
Jest żądanie → jest SecurityContext → w nim jest Authentication (albo przynajmniej zrozumiała zaślepka)
I to prowadzi nas do pytania: „A gdzie ten kontekst leży i jak go dostać?"
3. SecurityContextHolder: dostęp do kontekstu
Na poziomie odczuć SecurityContext przypomina „globalną zmienną bieżącego użytkownika". Ale Spring Security robi to starannie: nie proponuje ci przechowywania użytkownika we własnym statycznym polu – to byłby bilet do świata bólu. Zamiast tego jest oficjalny mechanizm dostępu: SecurityContextHolder.
SecurityContextHolder to punkt dostępu do kontekstu bezpieczeństwa podczas przetwarzania bieżącego żądania. Kluczowe słowo to „bieżącego". Kontekst nie powinien wyciekać między żądaniami, użytkownikami i wątkami. Spring Security buduje przetwarzanie tak, żeby w ramach jednego żądania miałeś poprawny kontekst, a po zakończeniu żądania nie zostawał on wisieć jak duch starego użytkownika.
Minimalny przykład „dotknąć rękami" wygląda tak (wewnątrz twojej aplikacji):
import org.springframework.security.core.context.SecurityContextHolder;
public class Demo {
public void printContext() {
// Pobieramy kontekst bezpieczeństwa bieżącego żądania/wątku
var context = SecurityContextHolder.getContext();
// Wydruk jest wygodny do diagnostyki: w prawdziwej logice zazwyczaj tak się nie robi
System.out.println(context); // Wyświetli obiekt kontekstu (zawartość zależy od żądania)
}
}
Tu ważne jest uchwycenie myśli: nie tworzysz SecurityContext ręcznie w zwykłej aplikacji. Przygotowuje go łańcuch security. Możesz jedynie w prawidłowym miejscu – np. w kontrolerze – sprawdzić, co jest teraz w środku.
I teraz główny zwrot dzisiejszej lektury: nawet jeśli użytkownik się nie zalogował, kontekst wciąż może mieć sens.
4. Anonymous authentication: stan zamiast null
Właśnie tu u początkujących często dochodzi do dysonansu kognitywnego: „Poczekajcie, chcecie powiedzieć, że «anonimowy użytkownik» to też Authentication?" Tak. Dokładnie tak Spring Security robi to domyślnie.
W Spring Security jest osobna konfiguracja anonymous authentication, której sens jest sformułowany bardzo wprost: umieścić w kontekście Authentication reprezentujący anonimowego użytkownika, zamiast null. W typowej konfiguracji robi to para AnonymousAuthenticationFilter i AnonymousAuthenticationProvider, i jest włączona domyślnie z rozsądnymi wartościami.
Dlaczego jest to wygodne? Bo dalej cały system działa bez „dziur" w modelu:
- filtry i kod autoryzacji mogą polegać na tym, że Authentication albo jest (i wtedy można patrzeć na authorities/rolę), albo naprawdę go nie ma w jakichś szczególnych scenariuszach (i to już sygnał o niezwykłej sytuacji);
- można jednolicie decydować „przepuścić dalej" lub „zatrzymać" żądanie;
- można rozdzielać scenariusze „gość" i „użytkownik", nie zamieniając tego w chaos if-ów.
Żeby anonimowość była nie tylko „pojęciem słownym", lecz weryfikowalnym stanem, Spring Security używa specjalnego typu Authentication: AnonymousAuthenticationToken. I zazwyczaj niesie on mniej więcej taką ideę: principal = anonymousUser i jakieś minimalne authorities, jak ROLE_ANONYMOUS.
Teraz nie trzeba zapamiętywać, jakie dokładnie stringi tam leżą. Ważniejsze jest co innego: anonimowość to nie brak stanu, lecz osobny stan.
Mały, ale ważny szczegół o Principal w Spring MVC
Intuicyjnie chce się sprawdzać anonimowość tak: „jeśli Principal w kontrolerze jest null – znaczy gość". Często to działa. Ale Spring Security ma tu niuans: to, jak Spring MVC wypełnia parametry kontrolera (Principal, Authentication), opiera się na HttpServletRequest#getPrincipal, a dla żądań anonimowych może on być null. Spring Security oficjalnie ostrzega o tym i proponuje pobieranie kontekstu inaczej, jeśli ważne jest zobaczenie anonimowości właśnie jako Authentication.
Na razie nie będziemy wchodzić w zaawansowane adnotacje, ale wniosek dla nas jest prosty: Principal jest wygodny, a SecurityContextHolder jest bardziej niezawodny jako „źródło prawdy", jeśli potrzebujesz właśnie stanu security.
5. Bieżący użytkownik w kontrolerze: Principal
Teraz będzie maksymalnie praktyczna część. W większości edukacyjnych i prawdziwych projektów prędzej czy później będziesz chciał endpoint „pokaż mi, kim jestem". W naszej domenie przypomina to /api/me, ale na razie robimy mały krok diagnostyczny, żeby zobaczyć sam mechanizm, a nie logikę biznesową profilu.
W Spring MVC można poprosić framework o przekazanie bieżącego użytkownika do metody kontrolera przez parametr Principal. Jeśli żądanie jest anonimowe, Principal często będzie null. Jeśli użytkownik się zalogował, Principal będzie miał nazwę (zazwyczaj username).
Zróbmy prosty endpoint:
import java.security.Principal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class WhoAmIController {
@GetMapping("/api/debug/whoami")
public String whoAmI(Principal principal) {
// Dla anonimowego żądania principal często będzie null – to oczekiwane zachowanie
return principal == null ? "guest" : principal.getName(); // Nazwa zazwyczaj odpowiada username
}
}
Co dostajesz jako programista? Bardzo prostą semantykę aplikacyjną: albo guest, albo nazwa użytkownika. Jest to przydatne, żeby przestać myśleć „a gdzie Spring Security przechowuje użytkownika?". Przechowuje go tak, żeby MVC mogło, jeśli potrzeba, przekazać ci to do kontrolera.
Przy tym ważne jest pamiętanie o granicy: ten endpoint nie powinien zamieniać się w twój „biznesowy API profilu". Jest diagnostyczny. Potrzebny jest po to, żeby tobie jako początkującemu inżynierowi stało się oczywiste: bieżący użytkownik to nie zmienna gdzieś w twoim serwisie, lecz część infrastruktury żądania.
6. Bieżący użytkownik przez SecurityContextHolder
Niekiedy Principal jest wygodny, ale niekiedy chcesz zobaczyć dokładnie to, co widzi warstwa security: jaki Authentication leży teraz w kontekście. Jest to szczególnie przydatne, żeby odróżnić „kontroler nie został wywołany" od „kontroler został wywołany, ale wewnątrz jest puste" – i nie urządzać dochodzenia w stylu „Sherlock Holmes i zaginiony endpoint".
Zróbmy drugi endpoint, który bierze informacje bezpośrednio z SecurityContextHolder:
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class WhoAmIFromContextController {
@GetMapping("/api/debug/whoami-context")
public String whoAmIFromContext() {
// Pobieramy Authentication wprost z SecurityContext bieżącego żądania
var auth = SecurityContextHolder.getContext().getAuthentication();
// Normalnie dla anonymous tu może być AnonymousAuthenticationToken, a nie null
return auth == null ? "no-authentication" : auth.getName();
}
}
Ten kod jest nieco mniej „ludzki", ale bardziej bezpośredni. Pokazuje główną ideę lektury: „bieżący użytkownik" dla aplikacji to to, co leży w SecurityContext bieżącego żądania.
A jeśli chcesz nie tylko nazwę, lecz i zrozumienie, „czy to żądanie jest anonimowe czy nie", możesz zrobić sprawdzenie na AnonymousAuthenticationToken:
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
public class AuthFlags {
public static boolean isAnonymous() {
// Pobieramy bieżący Authentication z kontekstu
var auth = SecurityContextHolder.getContext().getAuthentication();
// Ważne: "anonimowy" to osobny typ tokenu, a nie po prostu null
return auth instanceof AnonymousAuthenticationToken;
}
}
Zwróć uwagę na styl inżynierski: mała funkcja, zrozumiała nazwa, minimum szczegółów. Takie podejście pomaga nie ciągać po całym projekcie sprawdzeń z instanceof i nie zamieniać kodu w spaghetti ze sprawdzeń security.
Gdy ten obraz składa się w głowie, logi security zaczynają się czytać łatwiej. Zamiast abstrakcyjnego „coś poszło nie tak" rozumiesz już, jakiego Authentication oczekujesz w kontekście i dlaczego anonymous to normalny stan żądania, a nie usterka.
7. Użytkownik security i model domenowy
Ta sekcja zazwyczaj oszczędza studentom parę tygodni bólu (i jeden nerwowy tik oka). W Spring Security „użytkownik" wewnątrz Authentication to reprezentacja security. Odpowiada na pytania „kto przyszedł" i „jakie ma prawa/statusy", ale nie musi zawierać twojego prawdziwego profilu, biogramu, awataru, listy szkiców i ulubionego koloru przycisku „Opublikuj".
Dlaczego ważne jest to zrozumieć teraz, przed kolejnymi modułami? Bo inaczej początkujący zaczyna robić niebezpieczne rzeczy. Na przykład próbuje przechowywać „pełną encję User z bazy" bezpośrednio w SecurityContext na zawsze. Lub, jeszcze weseleją, wpychać tam to, co w ogóle nie ma związku z bezpieczeństwem (np. ustawienia UI). To niszczy granice odpowiedzialności.
Na bieżącym etapie kursu wystarczy zapamiętać regułę: SecurityContext i Authentication dają minimalnie wystarczające informacje do podejmowania decyzji security na poziomie żądania. A dane biznesowe to osobna warstwa. Dlatego gdy widzisz auth.getName(), traktuj to jako „identyfikator użytkownika w świecie security", a nie jako „wszystko, co wiem o człowieku".
Jeśli przeprowadzić analogię: Authentication to przepustka do biura, a UserProfile to akta osobowe pracownika. Przepustka jest potrzebna do przejścia przez kołowrót, ale kołowrót nie musi znać twojej biografii.
8. Typowe błędy przy pracy z anonymous authentication
Błąd nr 1: uważać, że anonymous user to „w ogóle niczego nie ma".
Początkujący często myślą, że przed zalogowaniem w aplikacji nie ma żadnego użytkownika, i dlatego można wszędzie pisać „jeśli użytkownika nie ma – robimy X". W Spring Security przydatniej jest myśleć inaczej: żądanie i tak przechodzi przez model security, a anonymous to osobny, normalny stan. Tak unikasz chaosu null i uzyskujesz jednolity styl podejmowania decyzji. I właśnie dla tego Spring Security domyślnie włącza anonymous authentication – żeby w kontekście był nie null, lecz sensowny Authentication anonimowego użytkownika.
Błąd nr 2: mylić Principal i Authentication jako „zawsze to samo".
Principal w kontrolerze to wygodny aplikacyjny sposób na uzyskanie nazwy użytkownika, ale nie zawsze odzwierciedla pełny obraz warstwy security. W szczególności w żądaniach anonimowych HttpServletRequest#getPrincipal może być null, i dlatego próby określania anonimowości tylko przez parametry kontrolera mogą zachowywać się nieoczekiwanie. Jeśli potrzebujesz dokładnej informacji warstwy security, lepiej patrzeć do SecurityContext. Ten niuans jest wprost opisany w dokumentacji Spring Security dla anonymous authentication i integracji z MVC.
Błąd nr 3: przechowywać bieżącego użytkownika we własnych zmiennych statycznych lub singletonach „dla pamięci".
Niekiedy studentowi wydaje się logiczne „raz przeczytać użytkownika z kontekstu i zapisać w polu serwisu". To niszczy całą ideę stanu request-scoped: następne żądanie może przyjść od innego użytkownika, a ty będziesz pracować ze starą wartością. Prawidłowy model: SecurityContextHolder to dostęp do kontekstu na czas bieżącego żądania, nie więcej.
Błąd nr 4: próbować wpychać do SecurityContext obiekty biznesowe „dla wygody".
Bardzo chce się położyć tam cały UserProfile, listę ról, awatar i ulubione artykuły, żeby nigdzie dalej nie chodzić. Ale SecurityContext dotyczy bezpieczeństwa, nie biznesu. Na tym etapie wystarczy traktować Authentication jak kompaktową „wizytówkę security": nazwa + prawa + status, ale nie jak „domenową encję użytkownika".
Błąd nr 5: wyciągać wniosek „kontroler nie został wywołany – znaczy bug w kontrolerze".
Jeśli endpoint jest nieosiągalny, początkujący często od razu zagląda do kontrolera, wstawia System.out.println("reached") i nie rozumie, dlaczego go nie ma. Ale właśnie w tym dniu składamy model, w którym żądanie może zostać zatrzymane w filtrach jeszcze przed MVC. Dlatego „kontroler nie został wywołany" to nie zagadka, lecz ważny fakt diagnostyczny: warstwa security mogła zakończyć przetwarzanie wcześniej, i to normalny element architektury.
GO TO FULL VERSION