«1. Введение

В этом руководстве мы познакомимся с AuthenticationManagerResolver, а затем покажем, как использовать его для потоков аутентификации Basic и OAuth2.

2. Что такое AuthenticationManager?

Проще говоря, AuthenticationManager — это основной интерфейс стратегии аутентификации.

Если принципал входной аутентификации действителен и проверен, AuthenticationManager#authenticate возвращает экземпляр аутентификации с флагом аутентификации, установленным в true. В противном случае, если принципал недействителен, будет выдано исключение AuthenticationException. В последнем случае он возвращает null, если не может принять решение.

ProviderManager — это реализация AuthenticationManager по умолчанию. Он делегирует процесс аутентификации списку экземпляров AuthenticationProvider.

Мы можем настроить глобальный или локальный AuthenticationManager, если расширим WebSecurityConfigurerAdapter. Для локального AuthenticationManager мы могли бы переопределить configure(AuthenticationManagerBuilder).

AuthenticationManagerBuilder — это вспомогательный класс, упрощающий настройку UserDetailService, AuthenticationProvider и других зависимостей для создания AuthenticationManager.

Для глобального AuthenticationManager мы должны определить AuthenticationManager как компонент.

3. Почему AuthenticationManagerResolver?

AuthenticationManagerResolver позволяет Spring выбирать AuthenticationManager для каждого контекста. Это новая функция, добавленная в Spring Security в версии 5.2.0:

public interface AuthenticationManagerResolver<C> {
    AuthenticationManager resolve(C context);
}

AuthenticationManagerResolver#resolve может возвращать экземпляр AuthenticationManager на основе общего контекста. Другими словами, мы можем установить класс в качестве контекста, если мы хотим разрешить AuthenticationManager в соответствии с ним.

Spring Security интегрировала AuthenticationManagerResolver в поток аутентификации с HttpServletRequest и ServerWebExchange в качестве контекста.

4. Сценарий использования

Давайте посмотрим, как использовать AuthenticationManagerResolver на практике.

Например, предположим, что в системе есть две группы пользователей: сотрудники и клиенты. Эти две группы имеют определенную логику проверки подлинности и отдельные хранилища данных. Более того, пользователям любой из этих групп разрешено вызывать только соответствующие URL-адреса.

5. Как работает AuthenticationManagerResolver?

Мы можем использовать AuthenticationManagerResolver везде, где нам нужно динамически выбирать AuthenticationManager, но в этом руководстве мы заинтересованы в использовании его во встроенных потоках аутентификации.

Сначала настроим AuthenticationManagerResolver, а затем используем его для аутентификации Basic и OAuth2.

5.1. Настройка AuthenticationManagerResolver

Давайте начнем с создания класса для настройки безопасности. Мы должны расширить WebSecurityConfigurerAdapter:

@Configuration
public class CustomWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    // ...
}

Затем добавим метод, который возвращает AuthenticationManager для клиентов:

AuthenticationManager customersAuthenticationManager() {
    return authentication -> {
        if (isCustomer(authentication)) {
            return new UsernamePasswordAuthenticationToken(/*credentials*/);
        }
        throw new UsernameNotFoundException(/*principal name*/);
    };
}

AuthenticationManager для сотрудников логически такой же, только мы заменяем isCustomer на isEmployee:

public AuthenticationManager employeesAuthenticationManager() {
    return authentication -> {
        if (isEmployee(authentication)) {
            return new UsernamePasswordAuthenticationToken(/*credentials*/);
        }
        throw new UsernameNotFoundException(/*principal name*/);
    };
}

~ ~~ Наконец, давайте добавим AuthenticationManagerResolver, который разрешается в соответствии с URL-адресом запроса:

AuthenticationManagerResolver<HttpServletRequest> resolver() {
    return request -> {
        if (request.getPathInfo().startsWith("/employee")) {
            return employeesAuthenticationManager();
        }
        return customersAuthenticationManager();
    };
}

5.2. Для базовой аутентификации

Мы можем использовать AuthenticationFilter для динамического разрешения AuthenticationManager для каждого запроса. AuthenticationFilter был добавлен в Spring Security в версии 5.2.

Если мы добавим его в нашу цепочку фильтров безопасности, то для каждого сопоставленного запроса он сначала проверит, может ли он извлечь какой-либо объект аутентификации или нет. Если да, то он запрашивает у AuthenticationManagerResolver подходящий AuthenticationManager и продолжает поток.

Во-первых, давайте добавим в наш CustomWebSecurityConfigurer метод для создания AuthenticationFilter:

private AuthenticationFilter authenticationFilter() {
    AuthenticationFilter filter = new AuthenticationFilter(
      resolver(), authenticationConverter());
    filter.setSuccessHandler((request, response, auth) -> {});
    return filter;
}

Причина установки AuthenticationFilter#successHandler с неактивным SuccessHandler состоит в том, чтобы предотвратить поведение перенаправления по умолчанию после успешной аутентификации.

Затем мы можем добавить этот фильтр в нашу цепочку фильтров безопасности, переопределив WebSecurityConfigurerAdapter#configure(HttpSecurity) в нашем CustomWebSecurityConfigurer:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.addFilterBefore(
      authenticationFilter(),
      BasicAuthenticationFilter.class);
}

5.3. Для аутентификации OAuth2

«BearerTokenAuthenticationFilter отвечает за аутентификацию OAuth2. Метод BearerTokenAuthenticationFilter#doFilterInternal проверяет BearerTokenAuthenticationToken в запросе и, если он доступен, разрешает соответствующий AuthenticationManager для аутентификации токена.

OAuth2ResourceServerConfigurer используется для настройки BearerTokenAuthenticationFilter.

Итак, мы можем настроить AuthenticationManagerResolver для нашего сервера ресурсов в нашем CustomWebSecurityConfigurer, переопределив WebSecurityConfigurerAdapter#configure(HttpSecurity):

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
      .oauth2ResourceServer()
      .authenticationManagerResolver(resolver());
}

6. Разрешить AuthenticationManager в реактивных приложениях

Для реактивного веб-приложения мы по-прежнему может извлечь выгоду из концепции разрешения AuthenticationManager в соответствии с контекстом. Но здесь вместо этого у нас есть ReactiveAuthenticationManagerResolver:

@FunctionalInterface
public interface ReactiveAuthenticationManagerResolver<C> {
    Mono<ReactiveAuthenticationManager> resolve(C context);
}

Он возвращает Mono из ReactiveAuthenticationManager. ReactiveAuthenticationManager является реактивным эквивалентом AuthenticationManager, поэтому его метод аутентификации возвращает Mono.

6.1. Настройка ReactiveAuthenticationManagerResolver

Начнем с создания класса для настройки безопасности:

@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class CustomWebSecurityConfig {
    // ...
}

Далее определим ReactiveAuthenticationManager для клиентов в этом классе:

ReactiveAuthenticationManager customersAuthenticationManager() {
    return authentication -> customer(authentication)
      .switchIfEmpty(Mono.error(new UsernameNotFoundException(/*principal name*/)))
      .map(b -> new UsernamePasswordAuthenticationToken(/*credentials*/));
}

И после этого определим ReactiveAuthenticationManager для сотрудников :

public ReactiveAuthenticationManager employeesAuthenticationManager() {
    return authentication -> employee(authentication)
      .switchIfEmpty(Mono.error(new UsernameNotFoundException(/*principal name*/)))
      .map(b -> new UsernamePasswordAuthenticationToken(/*credentials*/));
}

Наконец, мы настроили ReactiveAuthenticationManagerResolver на основе нашего сценария:

ReactiveAuthenticationManagerResolver<ServerWebExchange> resolver() {
    return exchange -> {
        if (match(exchange.getRequest(), "/employee")) {
            return Mono.just(employeesAuthenticationManager());
        }
        return Mono.just(customersAuthenticationManager());
    };
}

6.2. Для базовой аутентификации

В реактивном веб-приложении мы можем использовать AuthenticationWebFilter для аутентификации. Он аутентифицирует запрос и заполняет контекст безопасности.

AuthenticationWebFilter сначала проверяет соответствие запроса. После этого, если в запросе есть объект аутентификации, он получает подходящий ReactiveAuthenticationManager для запроса от ReactiveAuthenticationManagerResolver и продолжает процесс аутентификации.

Следовательно, мы можем настроить наш собственный AuthenticationWebFilter в нашей конфигурации безопасности:

@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
    return http
      .authorizeExchange()
      .pathMatchers("/**")
      .authenticated()
      .and()
      .httpBasic()
      .disable()
      .addFilterAfter(
        new AuthenticationWebFilter(resolver()), 
        SecurityWebFiltersOrder.REACTOR_CONTEXT
      )
      .build();
}

Сначала мы отключим ServerHttpSecurity#httpBasic, чтобы предотвратить нормальный поток аутентификации, затем вручную заменим его AuthenticationWebFilter, передав наш пользовательский преобразователь .

6.3. Для аутентификации OAuth2

Мы можем настроить ReactiveAuthenticationManagerResolver с помощью ServerHttpSecurity#oauth2ResourceServer. ServerHttpSecurity#build добавляет экземпляр AuthenticationWebFilter с нашим преобразователем в цепочку фильтров безопасности.

Итак, давайте настроим AuthenticationManagerResolver для фильтра аутентификации OAuth2 в нашей конфигурации безопасности:

@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
    return http
      // ...
      .and()
      .oauth2ResourceServer()
      .authenticationManagerResolver(resolver())
      .and()
      // ...;
}

7. Заключение

В этой статье мы использовали AuthenticationManagerResolver для аутентификации Basic и OAuth2 в рамках простого сценария.

Мы также изучили использование ReactiveAuthenticationManagerResolver в реактивных веб-приложениях Spring для аутентификации Basic и OAuth2.

Как всегда, исходный код доступен на GitHub. Наш реактивный пример также доступен на GitHub.