CodeGym /Kursy /Spring Security /AuthenticationProvider i DaoAuthenticationProvider

AuthenticationProvider i DaoAuthenticationProvider

Spring Security
Poziom 4, Lekcja 2
Dostępny

1. Od filtra do providera

Prawdziwa weryfikacja użytkownika żyje nie w managerze, lecz w providerze. AuthenticationManager pokazał już główne: przyjmuje Authentication attempt i uruchamia weryfikację. Ale sam manager nie szuka użytkownika, nie porównuje hasła i nie decyduje, jakie credentials uznać za poprawne. Jeśli nie wyodrębnić tego kawałka osobno, autentykacja znowu rozleje się po filtrach, kontrolerach i przypadkowych serwisach.

Dlatego następny węzeł łańcucha to AuthenticationProvider. Właśnie tu żyje prawdziwa weryfikacja: zrozumieć, czy potrafisz pracować z danym typem Authentication, i albo zwrócić potwierdzony wynik, albo odmówić. Dalej trzymamy fokus właśnie na tym miejscu, bez rozmywania providera w pełnym schemacie żądania.

2. AuthenticationProvider: kontrakt i odpowiedzialność

AuthenticationProvider to punkt, gdzie Spring Security oczekuje zobaczenia konkretnej weryfikacji. W terminach kodu to interfejs, który odpowiada na dwa pytania: „czy umiesz pracować z takim typem Authentication?" i „jeśli umiesz – sprawdź i zwróć wynik". Ważne, że provider żyje w świecie czystej Javy, a nie w świecie HTTP. To pozwala wielokrotnie używać tej samej weryfikacji w różnych scenariuszach webowych i nie przywiązywać bezpieczeństwa do kontrolerów.

Interfejs ma dwie kluczowe metody. Pierwsza – authenticate(...). Dostaje Authentication (zazwyczaj to próba logowania) i musi zwrócić pomyślny Authentication (już „authenticated") albo rzucić wyjątek AuthenticationException, jeśli weryfikacja nie przeszła. Druga metoda – supports(...): potrzebna po to, żeby ProviderManager mógł wybrać „właściwego specjalistę", a nie pukać do wszystkich z rzędu.

Minimalny szkielet providera wygląda tak (to nie „gotowe rozwiązanie", lecz forma myślenia):

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;

public class MyAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) {
        // WAŻNE: tu provider wykonuje prawdziwą weryfikację "tak/nie" (bez HTTP)
        // Teraz to zaślepka, żeby pokazać formę kontraktu.
        return null;
    }

    @Override
    public boolean supports(Class<?> authenticationType) {
        // WAŻNE: tu provider deklaruje, jakie typy Authentication potrafi obsługiwać
        // ProviderManager używa tego, żeby wybrać "właściwego specjalistę".
        return false;
    }
}

Teraz zróbmy edukacyjny „provider-parodię", żeby zobaczyć istotę mechaniki. Będzie przyjmował tylko login demo i hasło demo. W prawdziwej aplikacji tak robić nie wolno (i dobrze: inaczej wielu by tak zrobiło).

import java.util.List;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;

public Authentication authenticate(Authentication auth) {
    // principal zazwyczaj czyta się przez getName() / getPrincipal() – to "kto"
    String username = auth.getName();

    // credentials zazwyczaj leżą w getCredentials() – to "sekret" (np. hasło)
    Object credentials = auth.getCredentials();

    // Edukacyjna (i bardzo zła dla prawdziwego świata) weryfikacja
    if (!"demo".equals(username) || !"demo".equals(credentials)) {
        // Jeśli weryfikacja nie przeszła – rzucamy potomka AuthenticationException
        throw new BadCredentialsException("Bad credentials");
    }

    // W pomyślnym Authentication hasło zazwyczaj NIE jest przechowywane (dlatego tu null)
    return UsernamePasswordAuthenticationToken.authenticated("demo", null, List.of());
}

Tu ważne jest nie zapamiętanie „demo/demo", lecz zobaczenie formy. Provider bierze wejściowy Authentication, wydobywa principal (przez getName() lub getPrincipal()) i credentials (przez getCredentials()), sprawdza je i zwraca nowy Authentication, gdzie już jest znacznik „sprawdzono". Zwróć uwagę na null w drugim argumencie: w pomyślnym wyniku często nie przechowuje się hasła, żeby przypadkowo nie taszczać go dalej po kodzie. Hasło to nie pamiątka, nie trzeba nosić go ze sobą po aplikacji.

Kluczowa myśl jest prosta: provider to miejsce, gdzie żyje logika „tak / nie" dla autentykacji. Jeśli robić to w kontrolerze, rozmazywa się logikę po warstwie web. Jeśli robić to w filtrze, miesza się wydobywanie danych z HTTP i samą weryfikację. Spring Security jest inaczej zorganizowany: filtr zbiera próbę, provider sprawdza.

3. supports(...) i kilku providerów

Metoda supports(...) wydaje się nudną biurokracją, dopóki nie widzisz całego obrazu. Jest potrzebna dlatego, że providerów w aplikacji może być kilku, i każdy specjalizuje się w swoim typie Authentication. To jak zestaw różnych ładowarek: jedna pod USB-C, druga pod stare micro-USB, trzecia w ogóle do laptopa. Jeśli cały czas „wtykać dowolną ładowarkę gdziekolwiek", albo niczego nie naładujesz, albo psujesz złącze – a to już gorsze, ale życiowe.

Wewnątrz ProviderManager logika wyboru wygląda mniej więcej tak (uproszczenie, ale uczciwe co do sensu):

for (AuthenticationProvider provider : providers) {
    // ProviderManager najpierw filtruje providerów według typu Authentication
    if (provider.supports(authentication.getClass())) {
        // Znalazł "właściwego" – deleguje więc prawdziwą weryfikację do niego
        return provider.authenticate(authentication);
    }
}
// Jeśli nikt nie obsługuje tego typu Authentication – nie ma czym uwierzytelniać
throw new RuntimeException("No suitable provider");

Czyli ProviderManager to nie wróżka i nie jasnowidzący. Dosłownie idzie po liście providerów i pyta: „Ty to umiesz?". Umiesz – sprawdzaj. Nie – pomijam. To daje bardzo ważną swobodę inżynierską: warstwa web może tworzyć różne typy Authentication w zależności od scenariusza, a manager znajdzie providera, który umie z tym typem pracować.

Dla bieżącego kursu ważne jest zrozumienie: supports(...) to „jak provider deklaruje swoją specjalizację". Zazwyczaj w supports() zobaczysz sprawdzenie przez isAssignableFrom(...), żeby provider obsługiwał nie tylko dokładnie jedną klasę, ale i jej potomków:

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

public boolean supports(Class<?> type) {
    // isAssignableFrom(...) pozwala obsługiwać też potomków podanego typu
    return UsernamePasswordAuthenticationToken.class.isAssignableFrom(type);
}

I jeszcze jedna staranna myśl. Provider może być „ekspertem od weryfikacji tożsamości", ale nie powinien decydować, który URL jest dostępny dla kogo. Nawet jeśli zwrócił Authentication z jakimiś authorities, nie oznacza to „można robić wszystko". Oznacza to tylko „jesteśmy pewni, kto to jest". Autoryzacja żyje na innym poziomie, i jeśli zaczniesz wpychać „access rules" do providera, system stanie się jednocześnie nieprzewidywalny i trudny do testowania.

4. DaoAuthenticationProvider dla username/password

DaoAuthenticationProvider to najważniejszy provider kursu, bo implementuje podstawowy i bardzo popularny model: „użytkownik wpisuje login i hasło, serwer sprawdza". Jeśli AuthenticationProvider to abstrakcyjny „kontrakt eksperta", to DaoAuthenticationProvider to już konkretny ekspert, który umie pracować według standardowego schematu. Prawie zawsze najpierw używa się właśnie jego, a dopiero potem decyduje, czy trzeba coś rozszerzać.

Dlaczego w nazwie dao? Bo provider nie przechowuje użytkowników wewnątrz siebie. Ładuje dane o użytkowniku przez osobny komponent (w Spring Security to UserDetailsService). Czyli jego model jest taki: „daj mi login → pójdę do user store → dostanę informację o użytkowniku → sprawdzę hasło → zwrócę wynik". User store może być gdziekolwiek: w pamięci, w bazie danych, w LDAP (w naszym kursie LDAP nie jest potrzebny), ale idea pozostaje ta sama: provider nie wie, gdzie przechowujesz użytkowników, zna tylko kontrakt ładowania.

Na bieżącym kroku ważny nie jest cały flow żądania, lecz właśnie wewnętrzny kawałek pracy providera: dostaje już przygotowaną Authentication attempt, ładuje użytkownika, sprawdza hasło i zwraca wynik. W skrócie wygląda to tak:

flowchart TD
    A["Authentication attempt"] --> D["DaoAuthenticationProvider"]
    D --> E["UserDetailsService: załaduj użytkownika"]
    E --> U["UserDetails"]
    U --> D
    D --> F["PasswordEncoder: porównaj hasło"]
    F --> G["Authentication result"]

Tak, w schemacie jest PasswordEncoder. Nie omawialiśmy go jeszcze szczegółowo, ale tu ważne jest tylko miejsce: provider wykonuje weryfikację, a encoder daje sposób poprawnego porównania wprowadzonego hasła z tym, co jest przechowywane u użytkownika.

Jeszcze jeden przydatny szczegół: jeśli podłączyłeś spring-boot-starter-security i nic nie konfigurowałeś, jest duże prawdopodobieństwo, że gdzieś w głębi twojej aplikacji działa właśnie DaoAuthenticationProvider (w połączeniu z startowym UserDetailsService, który Spring Boot podnosi dla default user). Czyli studiujemy nie „rzadkie zwierzę", lecz typowy fundament.

5. Weryfikacje w DaoAuthenticationProvider

Gdy początkujący słyszy „sprawdzanie loginu i hasła", wyobraża to sobie jako jedną linijkę if(password.equals(...)). W rzeczywistości nawet w podstawowym modelu kroków jest więcej, i Spring Security robi je za ciebie, ale warto rozumieć sens. Nie będziemy wchodzić w kryptografię (to będzie osobny poziom), a po prostu rozłożymy weryfikacje według sensu – żeby w głowie pojawił się przejrzysty obraz „dlaczego może nie zadziałać".

Pierwsza klasa weryfikacji – „użytkownik istnieje i można go załadować". DaoAuthenticationProvider prosi UserDetailsService o znalezienie użytkownika po nazwie. Jeśli użytkownika nie ma, autentykacja nie może być uznana za pomyślną. Tu często ujawnia się typowy błąd: programista myśli, że problem jest z hasłem, a w rzeczywistości UserDetailsService po prostu nie znalazł użytkownika – przez literówkę, wielkość liter, zły identyfikator lub dlatego, że user store jest pusty.

Druga klasa weryfikacji – „hasło pasuje". Provider bierze to, co przyszło jako credentials (zazwyczaj surowe hasło z próby), i porównuje z tym, co jest przechowywane u użytkownika (zazwyczaj nie jest to surowe hasło, lecz zakodowana/zahaszowana wartość). Do tej części używa się PasswordEncoder. Dziś ważne jest dla ciebie tylko miejsce w architekturze: provider wykonuje weryfikację, encoder to „narzędzie porównania".

Trzecia klasa weryfikacji – „z kontem wszystko w porządku". Nawet jeśli login/hasło są prawidłowe, konto może być wyłączone, zablokowane i tak dalej. Te flagi pochodzą z UserDetails; tu ważny jest sam fakt: provider patrzy nie tylko na hasło, ale i na stan konta.

I na koniec, jeśli wszystko jest w porządku, provider tworzy wynik – Authentication, w którym jest już principal i authorities, i ten wynik trafi potem do SecurityContext. W pomyślnym wyniku provider zazwyczaj nie zostawia hasła, bo hasło jako sekret nie powinno wędrować po aplikacji.

Żeby zobaczyć różnicę „przed/po" w bardzo kompaktowej formie, można wyobrazić sobie taki kawałek kodu (to nie jest to, co będziesz pisać codziennie, to tylko ilustracja stanu):

import org.springframework.security.core.Authentication;

// attempt: to, co przyszło na wejście (zazwyczaj username+password)
Authentication attempt = /* username+password */;

// result: to, co zwraca provider przy sukcesie (principal+authorities)
Authentication result = /* authenticated principal+authorities */;

System.out.println(attempt.isAuthenticated()); // false
System.out.println(result.isAuthenticated());  // true

Jeśli trzymać w głowie właśnie taką różnicę, wiele „dziwnych" sytuacji w logach przestaje być mistyką. Masz próbę, masz wynik, i właśnie provider odpowiada za zamianę jednego w drugie.

6. Mini-połączenie: ProviderManager i DaoAuthenticationProvider

Gdy DaoAuthenticationProvider przestaje być abstrakcją, łatwiej postrzegać go jako zwykły Spring bean: jeden bean odpowiada za sam sposób weryfikacji, drugi składa AuthenticationManager ponad tym providerem. W Secure Content Platform API część security wygodnie trzymać pod com.example.securecontent.security....

Zaczniemy od providera. Chcemy jawnie pokazać: DaoAuthenticationProvider to AuthenticationProvider, który potrzebuje źródła użytkowników przez UserDetailsService. Dziś nie omawiamy, jak zbudowany jest UserDetailsService, więc po prostu przyjmujemy go jako zależność, którą Spring da nam z kontekstu (m.in. na etapie starter-security jest już tam bazowa implementacja).

package com.example.securecontent.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.userdetails.UserDetailsService;

@Configuration
public class SecurityProvidersConfig {

    @Bean
    AuthenticationProvider authenticationProvider(UserDetailsService users) {
        // Tworzymy standardowy provider dla username/password
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();

        // Podłączamy źródło użytkowników (skąd ładować UserDetails po username)
        provider.setUserDetailsService(users);

        // Uwaga: PasswordEncoder zazwyczaj konfiguruje się osobno (w innej konfiguracji)
        return provider;
    }
}

Od razu pojawia się następne pytanie. Provider potrzebuje nie abstrakcyjnego „użytkownika z domeny", lecz obiektu, w którym jest już username, password, authorities i stan konta w formie zrozumiałej dla Spring Security.

Zwróć uwagę na ważną „metodologiczną czystość" tej konstrukcji. Nie ma tu HTTP, nie ma kontrolerów, nie ma czytania nagłówków, nie ma JSON. Tylko provider i zależność „daj użytkowników". Właśnie tak powinno być: provider to nie warstwa web.

Teraz manager. W lekturze 2 omawialiśmy, że ProviderManager to typowa implementacja AuthenticationManager, która koordynuje jednego lub kilku providerów. W kodzie wygląda to bardzo nudno – a nudne w bezpieczeństwie często oznacza „prawidłowe".

package com.example.securecontent.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.AuthenticationProvider;

@Configuration
public class SecurityAuthenticationConfig {

    @Bean
    AuthenticationManager authenticationManager(AuthenticationProvider provider) {
        // AuthenticationManager to koordynacja, a nie "prawdziwa weryfikacja"
        // Deleguje ją wewnątrz do providera/providerów
        return new ProviderManager(provider);
    }
}

Zauważ: AuthenticationManager jest składany ponad AuthenticationProvider, a nie odwrotnie. To jeszcze raz podkreśla myśl dnia: manager nie żyje osobno od prawdziwej weryfikacji, on ją organizuje.

Jeśli chcesz „poczuć rękami", że providerzy naprawdę istnieją w kontekście, możesz dodać mały CommandLineRunner, który przy starcie wydrukuje listę providerów. To dobry edukacyjny trik: niczego nie psuje, ale sprawia, że abstrakcja staje się widoczna.

package com.example.securecontent.security.debug;

import java.util.List;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.stereotype.Component;

@Component
class AuthProvidersDebugRunner implements CommandLineRunner {

    // Spring zbierze tu wszystkie beany AuthenticationProvider z kontekstu
    private final List<AuthenticationProvider> providers;

    AuthProvidersDebugRunner(List<AuthenticationProvider> providers) {
        this.providers = providers;
    }

    @Override
    public void run(String... args) {
        // Edukacyjna diagnostyka: pokazać, jakie providerzy naprawdę się podniosły
        providers.forEach(p -> System.out.println(p.getClass().getSimpleName()));
        // przykładowe wyjście: DaoAuthenticationProvider
    }
}

Tak, w prawdziwym projekcie tak się nie robi (logi to osobna kultura), ale do nauki pomaga: masz poczucie, że provider to nie teoretyczny termin, lecz prawdziwy obiekt, który żyje w aplikacji i uczestniczy w autentykacji.

I jeszcze jedno ważne połączenie z projektem. W Secure Content Platform API będziemy mieć kilka stref: publiczna, osobista, redaktorska, adminowska. I żeby trafić choćby do jednej niepublicznej strefy, najpierw potrzeba uwierzytelnionego użytkownika. Tę część – „jak dostaje się uwierzytelnionym użytkownikiem" – właśnie teraz budujemy: filtr uruchamia proces, manager wybiera strategię, provider wykonuje prawdziwą weryfikację. Bez tego wszelkie rozmowy o dostępie do /api/me zamieniają się w dyskusję „czy można wpuścić człowieka do mieszkania, jeśli jeszcze nie wiemy, kto to".

7. Typowe błędy przy pracy z providerami

Zrozumienie providerów to jak nauczenie się odróżniania „przycisku włączającego" od „przedłużacza". Po tym wiele zagadkowych problemów przestaje być zagadkowymi, ale pojawiają się nowe – już inżynierskie. I to dobry znak: przestałeś wierzyć w magię i zacząłeś widzieć system.

Błąd nr 1: próbować sprawdzać login/hasło w kontrolerze lub serwisie.
Zazwyczaj zaczyna się od niewinnego if(password.equals(...)) w AuthController, a kończy się tym, że masz drugi kontroler, trzeci endpoint, i wszędzie kopiuje się logikę weryfikacji, obsługi błędów, blokad i tak dalej. Spring Security celowo robi providera centralnym miejscem weryfikacji, żebyś nie urządzał „security-copy-paste".

Błąd nr 2: mylić provider i filter według odpowiedzialności.
Filtr powinien wydobyć dane z żądania HTTP i stworzyć Authentication-próbę. Provider powinien sprawdzić, że próba jest poprawna. Gdy provider zaczyna parsować nagłówki, a filtr zaczyna chodzić do UserDetailsService, dostajesz wielowarstwowy tort, który rozwala się przy pierwszej zmianie scenariusza.

Błąd nr 3: błędnie zaimplementować supports(...).
Jeśli supports() zawsze zwraca true, twój provider będzie próbował obsługiwać absolutnie wszystko, włącznie z typami Authentication, do których nie jest przeznaczony. Jeśli supports() zawsze zwraca false, provider będzie „niewidzialny", i ProviderManager go po prostu nie wybierze. W obu przypadkach objawy wyglądają jak „dlaczego manager nie działa", choć problem polega na tym, że provider jest albo za zachłanny, albo za skromny.

Błąd nr 4: mieszać autentykację i autoryzację wewnątrz providera.
Provider odpowiada za „kto to", a nie za „co wolno". Może wkładać authorities do wyniku, żeby dalej system mógł podejmować decyzje, ale próba decydowania wewnątrz providera „czy wpuścić na /api/admin" – to błąd architektoniczny. Tracisz czytelność, wielokrotne użycie i normalną diagnostykę 401/403.

Błąd nr 5: napisać „edukacyjny" provider i przypadkowo zostawić go w projekcie jako prawdziwy.
Takie rzeczy jak demo/demo są przydatne do rozumienia mechaniki, ale niebezpieczne jako nawyk. W prawdziwej aplikacji provider musi opierać się na normalnym user store i normalnej weryfikacji hasła. Jeśli potrzebujesz „szybko zrobić logowanie", prawie zawsze lepiej jest użyć DaoAuthenticationProvider i poprawnie podłączyć UserDetailsService, niż pisać własnoręczne sprawdzenie.

1
Zadanie
Spring Security, poziom 4, lekcja 2
Niedostępne
Własny AuthenticationProvider z gałęziami success i failure
Własny AuthenticationProvider z gałęziami success i failure
1
Zadanie
Spring Security, poziom 4, lekcja 2
Niedostępne
Minimalna budowa DaoAuthenticationProvider
Minimalna budowa DaoAuthenticationProvider
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION