Skip to main content

Overview of core concepts in Spring Security

Jasne, oto wyjaśnienie kluczowych, wewnętrznych koncepcji Spring Security, skupione na najważniejszych klasach i interfejsach oraz ich roli.


Rdzenne Koncepcje Spring Security: Klasy i Ich Role​

Wyobraź sobie, że każde żądanie do Twojej aplikacji to osoba próbująca wejść do strzeżonego budynku. Spring Security zarządza tym procesem przy użyciu następujących komponentów:

1. SecurityFilterChain (Łańcuch Filtrów Bezpieczeństwa)​

Czym jest? To kręgosłup całego mechanizmu. Każde żądanie HTTP musi przejść przez ten łańcuch specjalistycznych filtrów, zanim dotrze do Twojego kontrolera. Do czego służy? W tym łańcuchu definiujesz reguły gry:

  • Które adresy URL są publiczne, a które wymagają logowania?
  • Jakie role są potrzebne do dostępu do konkretnych zasobów?
  • Jak obsługiwać logowanie, wylogowywanie, ataki CSRF czy sesje. To tutaj konfigurujesz CO jest chronione i JAK.

2. Authentication (Obiekt Uwierzytelnienia)​

Czym jest? To "paszport" użytkownika wewnątrz systemu po pomyślnym uwierzytelnieniu. Reprezentuje tożsamość i uprawnienia aktualnego podmiotu. Do czego służy? Przechowuje trzy kluczowe informacje:

  • Principal: Kim jest użytkownik (zazwyczaj obiekt UserDetails).
  • Credentials: Poświadczenia (np. hasło), które są zwykle czyszczone po udanym logowaniu.
  • Authorities (GrantedAuthority): Lista uprawnień (np. ROLE_ADMIN, READ_PRIVILEGE), które decydują o tym, co użytkownik może zrobić.

3. SecurityContextHolder i SecurityContext​

Czym są? SecurityContextHolder to globalnie dostępny "pojemnik" (wykorzystujący ThreadLocal), który przechowuje SecurityContext. Z kolei SecurityContext zawiera obiekt Authentication aktualnie zalogowanego użytkownika. Do czego służą? Dzięki nim możesz w dowolnym miejscu aplikacji (w serwisie, kontrolerze) uzyskać dostęp do informacji o zalogowanym użytkowniku, np. SecurityContextHolder.getContext().getAuthentication();. To standardowy sposób na sprawdzenie, kto wykonuje daną operację.

4. AuthenticationManager i AuthenticationProvider​

Czym są? To silnik uwierzytelniania.

  • AuthenticationManager: Główny menedżer, który otrzymuje prośbę o uwierzytelnienie. Nie wykonuje pracy sam, lecz deleguje ją do specjalistów.
  • AuthenticationProvider: Specjalista od konkretnego typu uwierzytelniania (np. na podstawie bazy danych, LDAP, tokena JWT). Manager pyta po kolei każdego zarejestrowanego Providera, czy jest w stanie zweryfikować dane poświadczenia. Do czego służą? Oddzielają logikę "zarządzania" procesem logowania od "wykonania" weryfikacji, co czyni system bardzo modularnym.

5. UserDetailsService i UserDetails​

Czym są? To standardowy sposób na pobieranie danych użytkownika ze źródła (np. bazy danych).

  • UserDetailsService: Serwis z jedną metodą: loadUserByUsername(String username). Jego jedynym zadaniem jest znalezienie użytkownika po nazwie.
  • UserDetails: "Teczka z aktami" użytkownika. Interfejs opisujący podstawowe dane potrzebne Spring Security: nazwa użytkownika, hasło (zaszyfrowane!), uprawnienia oraz status konta (czy jest aktywne, zablokowane itp.). Twoja klasa encji User zazwyczaj implementuje ten interfejs.

6. PasswordEncoder​

Czym jest? To prosty, ale absolutnie kluczowy interfejs do szyfrowania (a właściwie hashowania) haseł. Do czego służy? Zapewnia bezpieczne przechowywanie haseł. Jego dwie główne metody to:

  • encode(password): Tworzy bezpieczny hash z surowego hasła.
  • matches(rawPassword, encodedPassword): Porównuje surowe hasło (np. z formularza) z hashem zapisanym w bazie.

Podsumowanie Przepływu​

  1. Żądanie trafia do SecurityFilterChain.
  2. Odpowiedni filtr (np. do logowania) tworzy obiekt Authentication (z loginem/hasłem) i przekazuje go do AuthenticationManager.
  3. AuthenticationManager prosi AuthenticationProvider o weryfikację.
  4. AuthenticationProvider używa UserDetailsService, by pobrać dane użytkownika jako UserDetails.
  5. Następnie używa PasswordEncoder, by sprawdzić, czy podane hasło zgadza się z hashem z UserDetails.
  6. Jeśli wszystko się zgadza, tworzony jest w pełni uwierzytelniony obiekt Authentication, który trafia do SecurityContextHolder, skąd jest dostępny w całej aplikacji.

Oczywiście. Oto jeden, zwarty fragment kodu – klasa konfiguracyjna Spring Security – który w praktyce prezentuje relacje między kluczowymi obiektami.

Ten kod definiuje kompletne, minimalne zabezpieczenie aplikacji z dwoma użytkownikami przechowywanymi w pamięci.

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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

/**
* Główny komponent konfiguracji. Definiuje, jak chronione są zasoby aplikacji.
* To tutaj łączą się wszystkie elementy.
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// Reguły autoryzacji: dostęp do /admin/** wymaga roli "ADMIN"
.requestMatchers("/admin/**").hasRole("ADMIN")
// Dostęp do /profile/** wymaga roli "USER" (Admin też ją ma)
.requestMatchers("/profile/**").hasRole("USER")
// Strona główna jest dostępna dla wszystkich
.requestMatchers("/").permitAll()
// Wszystkie inne żądania wymagają uwierzytelnienia
.anyRequest().authenticated()
)
// Włącza domyślny formularz logowania, który użyje poniższych beanów
// do weryfikacji użytkownika.
.formLogin(Customizer.withDefaults());

return http.build();
}

/**
* Definiuje sposób pobierania danych użytkownika. Spring Security użyje tego serwisu,
* aby załadować dane (UserDetails) na podstawie nazwy użytkownika podanej w formularzu.
*/
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
// UserDetails: "teczka" z danymi użytkownika (login, hasło, role).
UserDetails user = User.withUsername("user")
.password(passwordEncoder.encode("password123")) // Hasło MUSI być zakodowane!
.roles("USER") // Rola "USER" zostanie przekonwertowana na uprawnienie "ROLE_USER"
.build();

UserDetails admin = User.withUsername("admin")
.password(passwordEncoder.encode("admin123"))
.roles("ADMIN", "USER") // Admin ma dwie role
.build();

// InMemoryUserDetailsManager to prosta implementacja UserDetailsService,
// która przechowuje użytkowników w pamięci.
return new InMemoryUserDetailsManager(user, admin);
}

/**
* Definiuje algorytm do hashowania haseł. Spring Security automatycznie
* użyje tego beana do porównywania haseł podczas logowania.
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

Jak te obiekty współpracują w tym kodzie:​

  1. Użytkownik wchodzi na /profile i zostaje przekierowany na stronę logowania (formLogin).
  2. Wpisuje "admin" i "admin123".
  3. Spring Security bierze te dane i szuka komponentu, który potrafi je zweryfikować (AuthenticationManager robi to za kulisami).
  4. AuthenticationManager używa skonfigurowanego przez nas UserDetailsService, aby pobrać dane o "adminie".
  5. Metoda userDetailsService() tworzy i zwraca obiekt UserDetails dla "admina", zawierający zahashowane hasło i role (ROLE_ADMIN, ROLE_USER).
  6. Następnie AuthenticationManager używa beana PasswordEncoder do porównania hasła admin123 z formularza z hashem pobranym z UserDetails.
  7. Ponieważ hasła się zgadzają, użytkownik zostaje pomyślnie uwierzytelniony.
  8. Na koniec, SecurityFilterChain sprawdza, czy uwierzytelniony użytkownik z rolą ROLE_ADMIN może wejść na /profile. Reguła .hasRole("USER") na to pozwala. Dostęp zostaje przyznany.

Doskonałe pytanie! Masz rację – te obiekty nie są definiowane jako @Bean, ponieważ nie są one statycznymi komponentami konfiguracji, lecz dynamicznymi obiektami tworzonymi w czasie rzeczywistym dla każdego żądania.

Reprezentują one stan uwierzytelnienia dla konkretnego użytkownika w danym momencie.

Oto jak te brakujące elementy układanki łączą się z kodem, który już poznałeś. Zamiast fragmentu kodu, pokażę Ci scenariusz działania, który najlepiej to zilustruje.


Scenariusz: Co się dzieje "za kulisami" podczas logowania?​

Wyobraźmy sobie, że użytkownik wysyła formularz logowania z username=user i password=password123.

Krok 1: Powstaje tymczasowy obiekt Authentication (bilet wstępu)

  • Jeden z filtrów w SecurityFilterChain (konkretnie UsernamePasswordAuthenticationFilter) przechwytuje żądanie.
  • Tworzy on pierwszą, niezweryfikowaną wersję obiektu Authentication.
  • W tym momencie obiekt ten zawiera:
  • principal: String "user" (nazwa użytkownika)
  • credentials: String "password123" (hasło)
  • isAuthenticated(): false

Krok 2: AuthenticationManager szuka wykonawcy

  • Filtr przekazuje ten tymczasowy obiekt Authentication do AuthenticationManager.
  • AuthenticationManager (który jest konfigurowany automatycznie, gdy dostarczasz UserDetailsService i PasswordEncoder) deleguje zadanie do odpowiedniego AuthenticationProvider. W naszym przypadku będzie to DaoAuthenticationProvider.

Krok 3: Weryfikacja i stworzenie docelowego obiektu Authentication

  • DaoAuthenticationProvider używa Twojego UserDetailsService, aby pobrać obiekt UserDetails dla użytkownika "user".
  • Następnie używa Twojego PasswordEncoder do porównania hasła password123 z hashem z obiektu UserDetails.
  • Jeśli wszystko się zgadza, Provider tworzy nowy, w pełni uwierzytelniony obiekt Authentication. To jest kluczowy moment! Ten obiekt zawiera już:
  • Principal: Obiekt UserDetails, który został pobrany z Twojego serwisu. To jest właśnie "podmiot" – kompletna reprezentacja użytkownika, a nie tylko jego nazwa.
  • Credentials: null (hasło jest natychmiast usuwane ze względów bezpieczeństwa).
  • Authorities: Kolekcja uprawnień (GrantedAuthority) skopiowana z UserDetails.
  • isAuthenticated(): true

Krok 4: Umieszczenie "paszportu" w globalnym kontekście

  • Ten nowy, uwierzytelniony obiekt Authentication jest zwracany do filtra.
  • Filtr umieszcza go w SecurityContext, który z kolei jest przechowywany w SecurityContextHolder.
  • SecurityContextHolder używa mechanizmu ThreadLocal, co oznacza, że obiekt Authentication jest dostępny w dowolnym miejscu aplikacji, ale tylko w obrębie wątku obsługującego bieżące żądanie.

Jak uzyskać dostęp do tych obiektów w kodzie?​

Teraz, gdy użytkownik jest zalogowany, możesz łatwo uzyskać dostęp do tych obiektów, na przykład w kontrolerze. To jest fragment kodu, który pokazuje, jak ich używać, a nie konfigurować.

import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProfileController {

@GetMapping("/profile/me")
public String getCurrentUserProfile(@AuthenticationPrincipal UserDetails userDetails) {
// SPOSÓB 1: Najlepszy i najprostszy (magia Springa)
// Spring automatycznie wstrzykuje tu obiekt Principal, czyli nasze UserDetails.
String username = userDetails.getUsername();
String roles = userDetails.getAuthorities().toString();

return "Hello, " + username + "! Your roles are: " + roles;
}

@GetMapping("/profile/me/verbose")
public String getCurrentUserVerbose() {
// SPOSÓB 2: Klasyczny (ręczne pobranie z kontekstu)
// Pobieramy cały obiekt Authentication z SecurityContextHolder.
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if (authentication != null && authentication.isAuthenticated()) {
// Principal to zazwyczaj obiekt UserDetails.
Object principal = authentication.getPrincipal();
String username;
if (principal instanceof UserDetails) {
username = ((UserDetails) principal).getUsername();
} else {
username = principal.toString();
}
return "Hello from verbose endpoint, " + username + "!";
}
return "No authenticated user found.";
}
}

Podsumowanie:​

  • Authentication, Principal, SecurityContext to obiekty runtime'owe, a nie konfiguracyjne.
  • Są one tworzone dynamicznie podczas procesu uwierzytelniania dla każdego żądania.
  • SecurityContextHolder działa jak globalny, ale bezpieczny wątkowo "schowek" na dane zalogowanego użytkownika.
  • Dostęp do nich uzyskujesz w kodzie aplikacji (np. w kontrolerze), aby dowiedzieć się, kto wykonuje operację i jakie ma uprawnienia.