JavaRush /Курси /Spring Security /Підключаємо DaoAuthenticat...

Підключаємо DaoAuthenticationProvider

Spring Security
Рівень 15 , Лекція 2
Відкрита

1. Коли CustomUserDetailsService не вистачає

Коли ви вперше написали CustomUserDetailsService, з’являється приємне відчуття: «Ну все, Spring Security тепер точно братиме користувачів із бази». Але одна важлива частина ще не зібрана: без явного провайдера автентифікація на основі БД залишається гарною заготовкою, а не перевіреним шляхом під час виконання. Ви можете вважати, що автентифікація йде через БД, а фактично вона йде через інше джерело, через механізм «за замовчуванням» або взагалі через старий in-memory, який залишився в проєкті «про всяк випадок».

Проблема тут не в тому, що Boot поганий або Spring Security «магічний». Проблема в тому, що security — це інфраструктура, де один зайвий бін може змінити поведінку сильніше, ніж десять рядків бізнес-логіки. Тому на цьому етапі курсу ми робимо крок до дорослого підходу: не сподіватися, що «якось само підчепиться», а явно сказати, який саме провайдер автентифікації ми використовуємо і які саме компоненти він має викликати.

Отже, нам потрібно зібрати це явно: пароль має перевірятися через PasswordEncoder, а користувач — завантажуватися через наш CustomUserDetailsService, який читає з бази.

Ментальна модель підключення

Перед тим як писати конфігурацію, корисно буквально проговорити вголос, що саме ми хочемо зібрати. Spring Security вже вміє username/password-автентифікацію. Ми не вигадуємо новий спосіб входу, ми просто змінюємо джерело даних користувача: замість in-memory — база даних.

У термінах об’єктів це виглядає так: коли користувачеві потрібно автентифікуватися (неважливо, через form login чи через HTTP Basic), Spring Security створює об’єкт Authentication із «сирою» парою username/password. Далі AuthenticationManager шукає відповідний AuthenticationProvider. У нашому випадку це DaoAuthenticationProvider. Він завантажує користувача через UserDetailsService і порівнює пароль через PasswordEncoder. Якщо все гаразд — повертає вже автентифікований Authentication разом із authorities, і далі це потрапляє в SecurityContext.

Для наочності — невелика схема (вона спеціально спрощена, без усіх фільтрів, інакше стала б схожою на схему метро в годину пік):

flowchart TD
    R["HTTP-запит із логіном і паролем"] --> F[Фільтри безпеки]
    F --> AM[AuthenticationManager]
    AM --> P[DaoAuthenticationProvider]
    P --> UDS[CustomUserDetailsService]
    UDS --> Repo[UserAccountRepository]
    P --> PE[PasswordEncoder]
    P --> OK["Автентифікований Authentication + authorities"]
    OK --> SC[SecurityContext]

Важливий зміст: CustomUserDetailsService — це не «система входу», а лише спосіб дістати користувача. А DaoAuthenticationProvider — це спосіб перевірити логін і пароль за стандартними правилами Spring Security.

2. DaoAuthenticationProvider: роль «перевірника на вході»

Тепер акуратно сформулюємо, навіщо нам саме DaoAuthenticationProvider і що він робить. Його назва починається з «Dao» не через моду на абревіатури, а через старий добрий патерн DAO (Data Access Object): провайдер, який працює зі сховищем користувачів через UserDetailsService. Тобто він не зобов’язаний знати нічого про JPA, SQL і таблиці. Його світ — це UserDetailsService і UserDetails.

Це корисно: ми можемо зберігати користувачів хоч у PostgreSQL, хоч у файлі, хоч у LDAP (в іншому курсі) — якщо в нас є UserDetailsService, для DaoAuthenticationProvider це все однаково. Він попросить користувача за логіном, отримає UserDetails і виконає перевірку.

У сценарії з БД нас цікавлять три речі.

По-перше, він візьме закодований пароль із UserDetails.getPassword() і порівняє його з «сирим» паролем із вхідного Authentication через PasswordEncoder.matches(raw, encoded). Це той момент, де дуже важливо не «допомогти» провайдеру своїм самописним порівнянням — інакше ви ламаєте модель безпеки й часто ще й хешування.

По-друге, він перевірить прапорці стану облікового запису, які ми перенесли з БД у UserDetails: disabled, locked та інші. На практиці це якраз ті поля, які в лекції про мапінг ми перенесли через disabled(...) і accountLocked(...).

По-третє, він повертає успішну автентифікацію так, щоб authorities із UserDetails стали частиною підсумкового Authentication. І саме ці authorities далі використовуються в hasRole(...), hasAuthority(...) і взагалі у всій авторизації.

І ось тут у нас з’являється головний висновок лекції: щоб DaoAuthenticationProvider почав працювати поверх бази, ми маємо явно дати йому два компоненти: UserDetailsService (наш на основі БД) і PasswordEncoder (наш обраний).

3. DaoAuthenticationProvider як бін

Зараз ми зробимо те, що виглядає нудно, але насправді є одним із найважливіших моментів курсу: акуратно зберемо конфігурацію так, щоб у Spring Security не було «простору для творчості». Це як підключення проводів у щитку: електрика й так існує, але якщо дроти встромити навмання — лампочка може вмикатися від чайника.

У проєкті ми зазвичай тримаємо такі речі в пакеті на кшталт com.example.securecontent.security.config. Зміст простий: SecurityFilterChain залишається читабельним, а внутрішня кухня автентифікації — в окремих бінах.

Найважливіший бін: DaoAuthenticationProvider

Почнемо з провайдера. Важливо: ми залежатимемо не від конкретного CustomUserDetailsService, а від інтерфейсу UserDetailsService. Це допомагає тримати конфігурацію більш гнучкою: сьогодні реалізація з БД, завтра — інша, але провайдеру все одно.

import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;

@Bean
DaoAuthenticationProvider daoAuthenticationProvider(
        UserDetailsService userDetailsService,
        PasswordEncoder passwordEncoder) {

    // Створюємо провайдера, який уміє стандартну автентифікацію за логіном і паролем
    // у зв’язці з UserDetailsService і PasswordEncoder.
    DaoAuthenticationProvider p = new DaoAuthenticationProvider();

    // Кажемо провайдеру, звідки саме завантажувати користувача за логіном.
    p.setUserDetailsService(userDetailsService);

    // Кажемо провайдеру, як порівнювати "сирий" пароль із закодованим паролем із БД.
    p.setPasswordEncoder(passwordEncoder);

    return p;
}

Зверніть увагу на логіку цього коду. Тут немає бази даних, немає SQL, немає JPA. Тут є лише: як завантажити користувача і як перевірити пароль. Усе інше — не справа провайдера.

Де взяти UserDetailsService

До цього моменту CustomUserDetailsService уже зареєстрований як @Service і реалізує UserDetailsService. Цього достатньо для основної гілки проєкту: другий UserDetailsService-бін нам не потрібен. Інакше ми знову створимо конкуруючі відповіді на запитання «хто саме завантажує користувача під час входу?».

Приклад (не повний, лише щоб освіжити в пам’яті форму класу):

import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;

@Service
public class CustomUserDetailsService implements UserDetailsService {
    // Усередині loadUserByUsername(...) ми:
    // 1) шукаємо користувача в репозиторії (БД),
    // 2) перетворюємо сутність на UserDetails (логін, хеш пароля, authorities, прапорці облікового запису).
}

Тобто дроти в нашому провайдері справді під’єднаються до потрібного місця: він отримає саме наш сервіс.

Де взяти PasswordEncoder

PasswordEncoder має бути оголошений як бін один раз і залишатися єдиним рішенням для проєкту. Зазвичай це було зроблено раніше, коли ми обирали стратегію кодування паролів. Якщо ви використовуєте DelegatingPasswordEncoder, це теж нормально: провайдеру все одно, він працює через спільний контракт.

Мінімальний приклад (якщо у вас ще немає, або щоб згадати форму):

import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Bean
PasswordEncoder passwordEncoder() {
    // Важливо: саме цей encoder має використовуватися і під час збереження пароля в БД,
    // і під час перевірки пароля під час входу (через PasswordEncoder.matches(...)).
    return new BCryptPasswordEncoder();
}

Тут ми не сперечаємося, який encoder кращий. Наше завдання — щоб він був і щоб він був один і той самий для хешів у БД і для порівняння під час входу.

Необов’язковий, але корисний вузол: AuthenticationManager і ProviderManager

Для поточного сценарію з БД це не обов’язкова частина збирання. Якщо провайдер уже зареєстрований у SecurityFilterChain, стандартний потік фільтрів може дійти до нього й без окремого біну AuthenticationManager. Але корисно розуміти, хто взагалі обирає провайдера і де в системі живе ця роль.

Коли дивишся на DaoAuthenticationProvider, виникає природне запитання: «Якщо він і так усе робить, навіщо ще якийсь AuthenticationManager?» Відповідь проста: у Spring Security роль менеджера — це роль диригента. У нього може бути кілька провайдерів (як мінімум теоретично), і він має вміти вибрати того, хто вміє обробити цей тип Authentication.

Навіть якщо в нас один провайдер, AuthenticationManager усе одно залишається центральною точкою, через яку запускається автентифікація. Усередині типового застосунку реалізацією менеджера є ProviderManager. Він просто зберігає список AuthenticationProvider і перебирає їх.

Нижче — чесна форма такого біну, якщо вам потрібно явно зібрати цей зв’язок в одному місці або передавати AuthenticationManager в інший інфраструктурний код:

import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;

@Bean
AuthenticationManager authenticationManager(DaoAuthenticationProvider provider) {
    // ProviderManager — стандартна реалізація AuthenticationManager,
    // яка просто перебирає список провайдерів.
    return new ProviderManager(provider);
}

Якщо вам зараз потрібен просто робочий auth на основі БД через наявний потік фільтрів входу, на цьому можна не ускладнювати конфігурацію далі: основний шлях уже зібраний на DaoAuthenticationProvider та його реєстрації в SecurityFilterChain. Явний AuthenticationManager стає корисним там, де автентифікацію потрібно запускати з коду, а не лише зі стандартного фільтра.

4. Підключення до SecurityFilterChain

Зараз найчастіший страх у студентів: «Якщо я почну все оголошувати явно, мій SecurityFilterChain розростеться до розмірів «Війни і миру»». Спойлер: не розростеться. Хороша security-конфігурація схожа на добре меню: коротка, зрозуміла, без несподіванок. Деталі — в «кухні» (тобто в інших конфігураційних бінах), а не в самому ланцюжку.

Є два здорові способи під’єднати провайдера так, щоб він точно використовувався.

Перший спосіб — зареєструвати його прямо в HttpSecurity через authenticationProvider(...). Це читається як «ось наш провайдер, використовуй його». Другий — задати AuthenticationManager. Ми оберемо перший, бо він зазвичай простіший візуально.

import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Bean
SecurityFilterChain securityFilterChain(
        HttpSecurity http,
        DaoAuthenticationProvider provider) throws Exception {

    return http
            // Явно реєструємо провайдера: тепер автентифікація йтиме через нього.
            .authenticationProvider(provider)

            // Правила авторизації тут для прикладу максимально прості.
            .authorizeHttpRequests(a -> a.anyRequest().authenticated())

            // Збираємо ланцюжок фільтрів.
            .build();
}

Зрозуміло, що в реальному проєкті замість anyRequest().authenticated() у вас уже є ваша матриця доступу (/api/public/** відкрито, /api/admin/** лише для адміністраторів тощо). Але важливий момент лекції не в матриці, а в тому, що ланцюжок залишається читабельним, а автентифікація тепер гарантовано використовує сховище користувачів на основі БД.

Тут важливо розрізняти дві речі: provider відповідає за перевірку користувача, а не за те, який саме фільтр приносить username/password. formLogin, httpBasic або інший механізм входу — це окремий шар ланцюжка. Для поточної теми нам важливо, що будь-який такий механізм тепер упиратиметься в наш провайдер на основі БД.

5. Єдине джерело істини: одне сховище користувачів

До цього місця в нас уже є DB-backed UserDetailsService, мапінг у UserDetails і провайдер. Якщо поруч залишити друге сховище користувачів, уся ця ланка знову розповзеться, і ви перестанете розуміти, хто саме відповідає за користувача в реальному потоці автентифікації.

Тепер поговоримо про ситуацію, яка в навчальних проєктах трапляється часто. Ви починали з in-memory користувачів, потім додали БД, і рука тягнеться залишити in-memory «про всяк випадок». Наприклад, щоб завжди був admin admin/admin навіть якщо база не піднялася. Звучить логічно, але для Spring Security це означає: у вас тепер два потенційні джерела користувачів і, найімовірніше, два провайдери (або один провайдер, який дивиться в одне UserDetailsService, а хтось підклав інше).

Проблема тут не в тому, що два джерела заборонені. Теоретично можна. Проблема в тому, що для рівня Junior це майже гарантована плутанина:

ви входите під одним і тим самим іменем, але різні середовища або гілки коду можуть повертати різні authorities; ви намагаєтеся змінити роль користувача в БД, а він і далі залишається ADMIN, бо вхід відбувається через in-memory; ви бачите користувача в таблиці, а Spring Security каже UsernameNotFoundException, бо запит пішов не туди.

Тому на цьому етапі курсу ми фіксуємо просте й корисне правило: після переходу на користувачів із БД саме БД стає основним джерелом істини. In-memory можна тримати лише як усвідомлений і явно відокремлений навчальний механізм, а не як другу рівноправну реальність.

З практичного погляду це означає: або ви видаляєте InMemoryUserDetailsManager, або робите так, щоб він не брав участі в реальному потоці автентифікації. Інакше ви самі собі влаштуєте «квест на 3 години» із запитанням: «Чому іноді 403, іноді 200, іноді мене перенаправляє, а іноді ні?».

6. Діагностика: перевіряємо, що провайдер працює

Після того як ви зібрали біни, корисно не просто сподіватися, що все запрацювало, а вміти швидко перевірити: чи справді автентифікація йде через мій DaoAuthenticationProvider і CustomUserDetailsService? Це не параноя, а нормальна інженерна звичка. У проєктах із безпекою самовпевненість — один із найдорожчих ресурсів.

Найпростіший спосіб — увімкнути debug-логи Spring Security. У навчальному проєкті це нормально: ми не в продакшені, ми вчимося бачити процес.

logging:
  level:
    # У навчальних цілях вмикаємо докладні логи Spring Security,
    # щоб бачити, який провайдер і які компоненти реально беруть участь в автентифікації.
    org.springframework.security: DEBUG

Ідея проста: під час спроби входу ви побачите, що запускається автентифікація, який провайдер використовується, і що UserDetailsService справді викликається. При цьому не треба логувати паролі й не треба витягати з логів токени — ми просто дивимося, що виклики йдуть правильною дорогою.

Ще один дуже людський спосіб перевірки — поставити акуратний лог у loadUserByUsername(...) вашого CustomUserDetailsService. Не треба друкувати пароль, звісно. Достатньо, наприклад, виводити повідомлення на кшталт «Пошук користувача за логіном…».

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

private static final Logger log =
        LoggerFactory.getLogger(CustomUserDetailsService.class);

// ...
// Логуємо лише факт пошуку користувача та його логін,
// без паролів, токенів та інших чутливих даних.
log.info("Завантаження користувача за логіном: {}", username);

Якщо під час входу в логах немає цього повідомлення — значить, CustomUserDetailsService взагалі не бере участі, і вам потрібно повернутися до конфігурації.

7. Типові помилки

У цій темі помилки особливо підступні: ви можете отримати 401 або 403 і почати лагодити правила доступу, хоча проблема насправді в автентифікації. Тому корисно заздалегідь знати найчастіші граблі — щоб наступати на них хоча б усвідомлено й з гідністю.

Помилка №1: провайдер створено, але PasswordEncoder не під’єднано.
Це виглядає майже невинно: ви зробили new DaoAuthenticationProvider(), задали UserDetailsService, а setPasswordEncoder(...) забули. У результаті пароль порівнюватиметься не тим способом, яким його було закодовано, і вхід стабільно ламатиметься. Особливо «весело», коли в БД лежить bcrypt-хеш, а провайдер раптом очікує інший формат.

Помилка №2: ви залишили поруч in-memory користувачів як рівноправне джерело, і тепер не розумієте, звідки береться користувач.
Ззовні це відчувається як «Spring Security живе своїм життям». Насправді він просто чесно йде по ланцюжку провайдерів, і ви самі дали йому два різні світи. У навчальному проєкті краще зробити одне джерело істини і не ускладнювати собі діагностику.

Помилка №3: CustomUserDetailsService є, але SecurityFilterChain використовує не його.
Так буває, якщо в контексті є інший UserDetailsService (наприклад, ви випадково залишили десь @Bean UserDetailsService ...), або якщо провайдер не зареєстровано чи не використовується. Тоді ви «бачите» свій сервіс у коді, але реальний потік автентифікації до нього не доходить. Лікується поверненням до явного збирання: провайдер + його реєстрація.

Помилка №4: ви зробили бін AuthenticationManager, але потім неявно зібрали інший менеджер в іншому місці.
Це рідкісніший, але неприємний випадок: в одній конфігурації ви створили ProviderManager, а в іншій — ще один. У підсумку частина фільтрів або частина механізму автентифікації може використовувати «не той» менеджер. На рівні fundamentals краще тримати одну явну конфігурацію, де всі security-біни зібрані в одному місці.

Помилка №5: очікувати, що DaoAuthenticationProvider сам «полагодить» ролі та права.
Провайдер не вигадує authorities. Він бере їх із UserDetails, який повернув UserDetailsService. Якщо UserDetails зібрано без ролей, автентифікація може пройти, але авторизація почне поводитися дивно: у користувача немає прав. Це не проблема провайдера — це сигнал, що security-представлення користувача зібране неповно. Тут важливо не почати «лікувати симптоми» в URL-правилах, а перевірити склад UserDetails.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ