«1. Обзор

В этой быстрой статье мы сосредоточимся на использовании нескольких механизмов для аутентификации пользователей в Spring Security.

Мы сделаем это, настроив несколько поставщиков аутентификации.

2. Поставщики аутентификации

AuthenticationProvider — это абстракция для получения информации о пользователе из определенного репозитория (например, из базы данных, LDAP, пользовательского стороннего источника и т. д.). Он использует полученную информацию о пользователе для проверки предоставленных учетных данных.

Проще говоря, когда определено несколько провайдеров аутентификации, провайдеры будут запрашиваться в том порядке, в котором они объявлены.

Для быстрой демонстрации мы настроим два провайдера аутентификации — пользовательский провайдер аутентификации и провайдер аутентификации в памяти.

3. Зависимости Maven

Давайте сначала добавим необходимые зависимости Spring Security в наше веб-приложение:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

И без Spring Boot:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
    <version>5.2.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-core</artifactId>
    <version>5.2.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
    <version>5.2.2.RELEASE</version>
</dependency>

Последнюю версию этих зависимостей можно найти в spring-security-web, spring-security-core и spring-security-config.

4. Пользовательский поставщик аутентификации

Давайте теперь создадим собственный поставщик аутентификации, реализовав интерфейс AuthneticationProvider.

Мы собираемся реализовать метод аутентификации, который пытается выполнить аутентификацию. Входной объект Authentication содержит учетные данные имени пользователя и пароля, предоставленные пользователем.

Метод authentication возвращает полностью заполненный объект Authentication, если аутентификация прошла успешно. Если аутентификация не удалась, выдается исключение типа AuthenticationException:

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication auth) 
      throws AuthenticationException {
        String username = auth.getName();
        String password = auth.getCredentials()
            .toString();

        if ("externaluser".equals(username) && "pass".equals(password)) {
            return new UsernamePasswordAuthenticationToken
              (username, password, Collections.emptyList());
        } else {
            throw new 
              BadCredentialsException("External system authentication failed");
        }
    }

    @Override
    public boolean supports(Class<?> auth) {
        return auth.equals(UsernamePasswordAuthenticationToken.class);
    }
}

Естественно, это простая реализация для нашего примера.

5. Настройка нескольких провайдеров аутентификации

Теперь добавим CustomAuthenticationProvider и провайдера аутентификации в памяти в нашу конфигурацию Spring Security.

5.1. Конфигурация Java

В нашем классе конфигурации давайте теперь создадим и добавим поставщиков аутентификации, используя AuthenticationManagerBuilder.

Сначала CustomAuthenticationProvider, а затем поставщик проверки подлинности в памяти с помощью inMemoryAuthentication().

Мы также обеспечиваем аутентификацию доступа к шаблону URL «/api/**»:

@EnableWebSecurity
public class MultipleAuthProvidersSecurityConfig 
  extends WebSecurityConfigurerAdapter {
    @Autowired
    CustomAuthenticationProvider customAuthProvider;

    @Override
    public void configure(AuthenticationManagerBuilder auth) 
      throws Exception {

        auth.authenticationProvider(customAuthProvider);
        auth.inMemoryAuthentication()
            .withUser("memuser")
            .password(encoder().encode("pass"))
            .roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.httpBasic()
            .and()
            .authorizeRequests()
            .antMatchers("/api/**")
            .authenticated();
    }
    
     
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

5.2. Конфигурация XML

В качестве альтернативы, если мы хотим использовать конфигурацию XML вместо конфигурации Java:

<security:authentication-manager>
    <security:authentication-provider>
        <security:user-service>
            <security:user name="memuser" password="pass" 
              authorities="ROLE_USER" />
        </security:user-service>
    </security:authentication-provider>
    <security:authentication-provider
      ref="customAuthenticationProvider" />
</security:authentication-manager>

<security:http>
    <security:http-basic />
    <security:intercept-url pattern="/api/**" 
      access="isAuthenticated()" />
</security:http>

6. Приложение

Далее давайте создадим простую конечную точку REST, защищенную двумя нашими поставщиками аутентификации.

Для доступа к этой конечной точке необходимо указать действительное имя пользователя и пароль. Наши поставщики аутентификации проверят учетные данные и определят, разрешать доступ или нет:

@RestController
public class MultipleAuthController {
    @GetMapping("/api/ping")
    public String getPing() {
        return "OK";
    }
}

7. Тестирование

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

@Autowired
private TestRestTemplate restTemplate;

@Test
public void givenMemUsers_whenGetPingWithValidUser_thenOk() {
    ResponseEntity<String> result 
      = makeRestCallToGetPing("memuser", "pass");

    assertThat(result.getStatusCodeValue()).isEqualTo(200);
    assertThat(result.getBody()).isEqualTo("OK");
}

@Test
public void givenExternalUsers_whenGetPingWithValidUser_thenOK() {
    ResponseEntity<String> result 
      = makeRestCallToGetPing("externaluser", "pass");

    assertThat(result.getStatusCodeValue()).isEqualTo(200);
    assertThat(result.getBody()).isEqualTo("OK");
}

@Test
public void givenAuthProviders_whenGetPingWithNoCred_then401() {
    ResponseEntity<String> result = makeRestCallToGetPing();

    assertThat(result.getStatusCodeValue()).isEqualTo(401);
}

@Test
public void givenAuthProviders_whenGetPingWithBadCred_then401() {
    ResponseEntity<String> result 
      = makeRestCallToGetPing("user", "bad_password");

    assertThat(result.getStatusCodeValue()).isEqualTo(401);
}

private ResponseEntity<String> 
  makeRestCallToGetPing(String username, String password) {
    return restTemplate.withBasicAuth(username, password)
      .getForEntity("/api/ping", String.class, Collections.emptyMap());
}

private ResponseEntity<String> makeRestCallToGetPing() {
    return restTemplate
      .getForEntity("/api/ping", String.class, Collections.emptyMap());
}

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

В этом кратком руководстве мы увидели, как можно настроить несколько поставщиков аутентификации в Spring Security. Мы защитили простое приложение с помощью собственного поставщика аутентификации и поставщика аутентификации в памяти.

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

Как всегда, полный исходный код реализации можно найти на GitHub.