JavaRush /Курсы /Spring Security /AuthenticationProvider

AuthenticationProvider и DaoAuthenticationProvider

Spring Security
4 уровень , 2 лекция
Открыта

1. От фильтра к провайдеру

Реальная проверка пользователя живёт не в manager, а в provider. AuthenticationManager уже показал главное: он принимает Authentication attempt и запускает проверку. Но сам manager не ищет пользователя, не сравнивает пароль и не решает, какие credentials считать валидными. Если этот кусок не выделить отдельно, аутентификация снова расползётся по фильтрам, контроллерам и случайным сервисам.

Поэтому следующий узел цепочки — AuthenticationProvider. Именно здесь живёт реальная проверка: понять, умеешь ли ты работать с таким типом Authentication, и либо вернуть подтверждённый результат, либо отказать. Дальше держим фокус именно на этом месте, без растворения провайдера в полной схеме запроса.

2. AuthenticationProvider: контракт и ответственность

AuthenticationProvider — это точка, где Spring Security ожидает увидеть конкретную проверку. В терминах кода это интерфейс, который отвечает на два вопроса: «умеешь ли ты работать с таким типом Authentication?» и «если умеешь — проверь и верни результат». Важно, что провайдер живёт в мире чистой Java, а не в мире HTTP. Это позволяет переиспользовать одну и ту же проверку в разных web-сценариях и не привязывать безопасность к контроллерам.

У интерфейса два ключевых метода. Первый — authenticate(...). Он получает Authentication (обычно это попытка входа) и должен вернуть успешный Authentication (уже “authenticated”) или бросить исключение AuthenticationException, если проверка не прошла. Второй метод — supports(...): он нужен, чтобы ProviderManager мог выбрать «подходящего специалиста», а не стучаться ко всем подряд.

Мини‑скелет провайдера выглядит вот так (это не «готовое решение», а форма мысли):

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

public class MyAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) {
        // ВАЖНО: здесь провайдер выполняет реальную проверку "да/нет" (без HTTP)
        // Сейчас это заглушка, чтобы показать форму контракта.
        return null;
    }

    @Override
    public boolean supports(Class
   authenticationType) {
        // ВАЖНО: здесь провайдер объявляет, какие типы Authentication он умеет обрабатывать
        // ProviderManager использует это, чтобы выбрать "подходящего специалиста".
        return false;
    }
}

Теперь давайте сделаем учебный «провайдер‑пародию», чтобы увидеть суть механики. Он будет принимать только логин demo и пароль demo. В реальном приложении так делать нельзя (и да, это прекрасно: иначе многие бы так и сделали).

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 обычно читают через getName() / getPrincipal() — это "кто"
    String username = auth.getName();

    // credentials обычно лежат в getCredentials() — это "секрет" (например, пароль)
    Object credentials = auth.getCredentials();

    // Учебная (и очень плохая для реального мира) проверка
    if (!"demo".equals(username) || !"demo".equals(credentials)) {
        // Если проверка не прошла — кидаем AuthenticationException-потомка
        throw new BadCredentialsException("Bad credentials");
    }

    // В успешном Authentication пароль обычно НЕ хранят (поэтому тут null)
    return UsernamePasswordAuthenticationToken.authenticated("demo", null, List.of());
}

Здесь важно не запомнить “demo/demo”, а увидеть форму. Провайдер берёт входной Authentication, достаёт principal (через getName() или getPrincipal()) и credentials (через getCredentials()), проверяет их и возвращает новый Authentication, где уже есть признак «проверено». Обратите внимание на null во втором аргументе: в успешном результате часто не хранят пароль, чтобы случайно не утянуть его дальше по коду. Пароль — это не сувенир, его не нужно носить с собой по приложению.

Ключевая мысль здесь простая: провайдер — это место, где живёт логика «да / нет» по аутентификации. Если делать это в контроллере, вы размазываете логику по web-слою. Если делать это в фильтре, вы смешиваете извлечение данных из HTTP и саму проверку. Spring Security устроен иначе: фильтр собирает попытку, провайдер проверяет.

3. supports(...) и несколько провайдеров

Метод supports(...) кажется скучной бюрократией, пока вы не увидите картину целиком. Он нужен потому, что провайдеров в приложении может быть несколько, и каждый специализируется на своём типе Authentication. Это как набор разных зарядок: одна под USB‑C, другая под старый micro‑USB, третья вообще для ноутбука. Если всё время «пихать любую зарядку куда попало», вы либо ничего не зарядите, либо сломаете разъём — а это уже хуже, но жизненно.

Внутри ProviderManager логика выбора выглядит примерно так (упрощённо, но честно по смыслу):

for (AuthenticationProvider provider : providers) {
    // ProviderManager сначала фильтрует провайдеры по типу Authentication
    if (provider.supports(authentication.getClass())) {
        // Нашёл "подходящего" — значит, делегирует реальную проверку ему
        return provider.authenticate(authentication);
    }
}
// Если никто не поддерживает этот тип Authentication — аутентифицировать нечем
throw new RuntimeException("No suitable provider");

То есть ProviderManager — не гадалка и не экстрасенс. Он буквально идёт по списку провайдеров и спрашивает: «Ты это умеешь?». Если умеешь — проверяй. Если нет — пропускаю. Это даёт очень важную инженерную свободу: web-уровень может создавать разные типы Authentication в зависимости от сценария, а менеджер найдёт провайдера, который умеет с этим типом работать.

Для текущего курса нам принципиально важно понять: supports(...) — это «как провайдер объявляет свою специализацию». Обычно в supports() вы увидите проверку через isAssignableFrom(...), чтобы провайдер поддерживал не только ровно один класс, но и его наследников:

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;

public boolean supports(Class
   type) {
    // isAssignableFrom(...) позволяет поддерживать и наследников указанного типа
    return UsernamePasswordAuthenticationToken.class.isAssignableFrom(type);
}

И ещё одна аккуратная мысль. Провайдер может быть «экспертом по проверке личности», но он не должен решать, какой URL кому доступен. Даже если он вернул Authentication с какими-то authorities, это ещё не означает «можно делать всё». Это просто означает «мы уверены, кто это такой». Авторизация живёт на другом уровне, и если вы начнёте засовывать “access rules” в провайдер, система станет одновременно и непредсказуемой, и плохо тестируемой.

4. DaoAuthenticationProvider для username/password

DaoAuthenticationProvider — это самый важный провайдер курса, потому что он реализует базовую и очень распространённую модель: «пользователь вводит логин и пароль, сервер проверяет». Если AuthenticationProvider — это абстрактный «контракт эксперта», то DaoAuthenticationProvider — уже конкретный эксперт, который умеет работать по стандартной схеме. Почти всегда вы сначала используете именно его, а уже потом решаете, нужно ли что-то расширять.

Почему в названии dao? Потому что провайдер не хранит пользователей внутри себя. Он загружает данные о пользователе через отдельный компонент (в Spring Security это UserDetailsService). То есть его модель такая: «дай мне логин → я схожу в user store → получу информацию о пользователе → проверю пароль → верну результат». User store может быть где угодно: в памяти, в базе данных, в LDAP (в нашем курсе LDAP не нужен), но идея остаётся той же: провайдер не знает, где вы храните пользователей, он знает только контракт загрузки.

Для текущего шага нам важен не весь запрос целиком, а именно внутренний кусок работы провайдера: он получает уже подготовленную Authentication attempt, загружает пользователя, проверяет пароль и возвращает результат. В сжатом виде это выглядит так:

flowchart TD
    A["Authentication attempt"] --> D["DaoAuthenticationProvider"]
    D --> E["UserDetailsService: загрузить пользователя"]
    E --> U["UserDetails"]
    U --> D
    D --> F["PasswordEncoder: сравнить пароль"]
    F --> G["Authentication result"]

Да, в схеме есть PasswordEncoder. Мы ещё не разбирали его детально, но здесь важно только место: провайдер делает проверку, а encoder даёт способ корректно сравнить введённый пароль с тем, что хранится у пользователя.

Ещё одна полезная деталь: если вы подключили spring-boot-starter-security и ничего не настраивали, высока вероятность, что где-то в недрах вашего приложения уже работает именно DaoAuthenticationProvider (в связке со стартовым UserDetailsService, который Spring Boot поднимает для default user). То есть мы изучаем не «редкого зверя», а типичный фундамент.

5. Проверки в DaoAuthenticationProvider

Когда новички слышат «проверка логина и пароля», они представляют это как одну строчку if(password.equals(...)). В реальности даже в базовой модели шагов больше, и Spring Security делает их за вас, но понимать смысл полезно. Мы не будем уходить в криптографию (это будет отдельный уровень), а просто разложим проверки по смыслу — чтобы в голове появилась прозрачная картина «почему может не сработать».

Первый класс проверок — «пользователь существует и его можно загрузить». DaoAuthenticationProvider просит UserDetailsService найти пользователя по имени. Если пользователя нет, аутентификация не может считаться успешной. Здесь часто проявляется типичная ошибка: разработчик думает, что проблема в пароле, а на деле UserDetailsService просто не нашёл пользователя — из-за опечатки, регистра, неправильного identifier или потому что user store пуст.

Второй класс проверок — «пароль подходит». Провайдер берёт то, что пришло как credentials (обычно raw пароль из попытки), и сравнивает с тем, что хранится у пользователя (обычно это не raw пароль, а закодированное/захешированное значение). Для этой части используется PasswordEncoder. Сегодня вам важно только место в архитектуре: провайдер делает проверку, encoder — это «инструмент сравнения».

Третий класс проверок — «с аккаунтом всё в порядке». Даже если логин/пароль верные, аккаунт может быть отключён, заблокирован и так далее. Эти флаги приходят из UserDetails; здесь нам важен сам факт: провайдер смотрит не только на пароль, но и на состояние аккаунта.

И наконец, если всё хорошо, провайдер создаёт результатAuthentication, где уже лежит principal и authorities, и этот результат потом попадёт в SecurityContext. В успешном результате провайдер обычно не оставляет пароль, потому что пароль как секрет не должен путешествовать по приложению.

Чтобы увидеть разницу «до/после» в очень компактном виде, можно представить такой кусочек кода (это не то, что вы будете писать каждый день, это просто иллюстрация состояния):

import org.springframework.security.core.Authentication;

// attempt: то, что пришло на вход (обычно username+password)
Authentication attempt = /* username+password */;

// result: то, что возвращает провайдер при успехе (principal+authorities)
Authentication result = /* authenticated principal+authorities */;

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

Если держать в голове именно такую разницу, многие «странные» ситуации в логах перестают быть мистикой. У вас есть попытка, у вас есть результат, и ровно провайдер отвечает за превращение одного в другое.

6. Мини‑связка: ProviderManager и DaoAuthenticationProvider

Когда DaoAuthenticationProvider перестаёт быть абстракцией, его проще воспринимать как обычный Spring bean: один бин отвечает за сам способ проверки, другой собирает AuthenticationManager поверх этого провайдера. В Secure Content Platform API security-часть удобно держать под com.example.securecontent.security....

Начнём с провайдера. Мы хотим явно показать: DaoAuthenticationProvider — это AuthenticationProvider, которому нужен источник пользователей через UserDetailsService. Сегодня мы не разбираем, как устроен UserDetailsService, поэтому просто принимаем его как зависимость, которую Spring даст нам из контекста (в том числе на этапе starter-security там уже есть базовая реализация).

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) {
        // Создаём стандартный провайдер для username/password
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();

        // Подключаем источник пользователей (откуда грузить UserDetails по username)
        provider.setUserDetailsService(users);

        // Примечание: PasswordEncoder обычно настраивается отдельно (в другой конфигурации)
        return provider;
    }
}

Здесь сразу всплывает следующий вопрос. Провайдеру нужен не абстрактный «пользователь из домена», а объект, в котором уже есть username, password, authorities и состояние аккаунта в форме, понятной Spring Security.

Обратите внимание на важную «методическую чистоту» этой конструкции. Здесь нет HTTP, нет контроллеров, нет чтения заголовков, нет JSON. Только провайдер и зависимость «дай пользователей». Именно так и должно быть: провайдер — не web‑слой.

Теперь менеджер. В лекции 2 мы обсуждали, что ProviderManager — типичная реализация AuthenticationManager, которая координирует одного или нескольких провайдеров. В коде это выглядит очень скучно — а скучное в безопасности часто означает «правильное».

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 — это координация, а не "реальная проверка"
        // Он делегирует её внутрь провайдера(ов)
        return new ProviderManager(provider);
    }
}

Заметьте: AuthenticationManager собирается поверх AuthenticationProvider, а не наоборот. Это ещё раз подчёркивает мысль дня: manager не живёт отдельно от реальной проверки, он её организует.

Если вы хотите «пощупать руками», что провайдеры реально существуют в контексте, можно добавить маленький CommandLineRunner, который на старте распечатает список провайдеров. Это хороший учебный трюк: он ничего не ломает, но делает абстракцию видимой.

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 соберёт сюда все AuthenticationProvider бины из контекста
    private final List<AuthenticationProvider> providers;

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

    @Override
    public void run(String... args) {
        // Учебная диагностика: показать, какие провайдеры реально поднялись
        providers.forEach(p -> System.out.println(p.getClass().getSimpleName()));
        // пример вывода: DaoAuthenticationProvider
    }
}

Да, в реальном проекте вы так делать не будете (логи — отдельная культура), но для обучения это помогает: у вас появляется ощущение, что провайдер — не теоретический термин, а реальный объект, который живёт в приложении и участвует в аутентификации.

И ещё одна важная связка с проектом. В Secure Content Platform API у нас будет несколько зон: публичная, личная, редакторская, админская. И чтобы попасть хотя бы в одну непубличную зону, нужно сначала получить аутентифицированного пользователя. Эту часть — «как получается аутентифицированный пользователь» — мы и строим сейчас: фильтр запускает процесс, manager выбирает стратегию, provider делает реальную проверку. Без этого любые разговоры про доступ к /api/me превращаются в спор «а можно ли пускать человека в квартиру, если мы ещё не знаем, кто он».

7. Типичные ошибки при работе с провайдерами

Разобраться в провайдерах — это как научиться отличать «кнопку включения» от «удлинителя». После этого многие загадочные проблемы перестают быть загадочными, но появляются новые — уже инженерные. И это хороший знак: вы перестали верить в магию и начали видеть систему.

Ошибка №1: пытаться проверять логин/пароль в контроллере или сервисе.
Обычно это начинается с безобидного if(password.equals(...)) в AuthController, а заканчивается тем, что у вас появляется второй контроллер, третий endpoint, и везде копируется логика проверки, обработки ошибок, блокировок и прочего. Spring Security специально делает провайдера центральным местом проверки, чтобы вы не устраивали “security‑копипасту”.

Ошибка №2: путать provider и filter по ответственности.
Фильтр должен извлечь данные из HTTP‑запроса и создать Authentication‑попытку. Провайдер должен проверить, что попытка валидна. Когда провайдер начинает разбирать заголовки, а фильтр начинает ходить в UserDetailsService, вы получаете слоёный пирог, который разваливается при первом же изменении сценария.

Ошибка №3: неверно реализовать supports(...).
Если supports() всегда возвращает true, ваш провайдер будет пытаться обработать вообще всё, включая те Authentication‑типы, для которых он не предназначен. Если supports() всегда возвращает false, провайдер будет «невидимкой», и ProviderManager его просто не выберет. В обоих случаях симптомы выглядят как «почему manager не работает», хотя проблема в том, что провайдер либо слишком жадный, либо слишком скромный.

Ошибка №4: смешивать аутентификацию и авторизацию внутри провайдера.
Провайдер отвечает за «кто ты», а не за «что можно». Он может положить authorities в результат, чтобы дальше система могла принимать решения, но пытаться внутри провайдера решать «пускать ли на /api/admin» — это архитектурная ошибка. Вы потеряете читаемость, переиспользуемость и нормальную диагностику 401/403.

Ошибка №5: писать «учебный» провайдер и случайно оставить его в проекте как реальный.
Такие штуки, как demo/demo, полезны для понимания механики, но опасны как привычка. В реальном приложении провайдер должен опираться на нормальный user store и нормальную проверку пароля. Если вам нужно «быстро сделать вход», почти всегда правильнее использовать DaoAuthenticationProvider и корректно подключить UserDetailsService, чем писать самодельную проверку.

1
Задача
Spring Security, 4 уровень, 2 лекция
Недоступна
Собственный AuthenticationProvider с success и failure ветками
Собственный AuthenticationProvider с success и failure ветками
1
Задача
Spring Security, 4 уровень, 2 лекция
Недоступна
Минимальная сборка DaoAuthenticationProvider
Минимальная сборка DaoAuthenticationProvider
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ