Рубрики
Без рубрики

Дополнительные поля входа в систему с Spring Security

Краткое и практическое руководство по добавлению дополнительных полей входа в систему.

Автор оригинала: Chris Oberle.

1. введение

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

Мы собираемся сосредоточиться на 2 различных подходах , чтобы показать универсальность фреймворка и гибкие способы его использования.

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

Наш второй подход будет более индивидуальным решением, которое может быть более подходящим для продвинутых вариантов использования.

Мы будем опираться на концепции, которые обсуждались в наших предыдущих статьях о Spring Security login .

2. Настройка Maven

Мы будем использовать Spring Boot starters для начальной загрузки нашего проекта и приведения всех необходимых зависимостей.

Настройка, которую мы будем использовать, требует родительского объявления, веб-стартера и стартера безопасности; мы также включим thymeleaf:


    org.springframework.boot
    spring-boot-starter-parent
    2.4.0
    

 

    
        org.springframework.boot
        spring-boot-starter-web
    
    
        org.springframework.boot
        spring-boot-starter-security
    
    
        org.springframework.boot
        spring-boot-starter-thymeleaf
     
     
        org.thymeleaf.extras
        thymeleaf-extras-springsecurity5
    

Самую последнюю версию Spring Boot security starter можно найти на сайте Maven Central .

3. Простая Настройка Проекта

В нашем первом подходе мы сосредоточимся на повторном использовании реализаций, предоставляемых Spring Security. В частности, мы будем повторно использовать DaoAuthenticationProvider и UsernamePasswordToken , поскольку они существуют “из коробки”.

Ключевые компоненты будут включать в себя:

  • SimpleAuthenticationFilter расширение UsernamePasswordAuthenticationFilter
  • SimpleUserDetailsService реализация UserDetailsService
  • Us er расширение класса User , предоставляемого Spring Security, которое объявляет наше дополнительное доменное поле
  • Securi tyConfig наша весенняя конфигурация безопасности, которая вставляет наш SimpleAuthenticationFilter в цепочку фильтров, объявляет правила безопасности и подключает зависимости
  • login.html |/– страница входа в систему, которая собирает имя пользователя , пароль и домен

3.1. Простой Фильтр Аутентификации

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

Затем токен передается поставщику аутентификации для аутентификации :

public class SimpleAuthenticationFilter
  extends UsernamePasswordAuthenticationFilter {

    @Override
    public Authentication attemptAuthentication(
      HttpServletRequest request, 
      HttpServletResponse response) 
        throws AuthenticationException {

        // ...

        UsernamePasswordAuthenticationToken authRequest
          = getAuthRequest(request);
        setDetails(request, authRequest);
        
        return this.getAuthenticationManager()
          .authenticate(authRequest);
    }

    private UsernamePasswordAuthenticationToken getAuthRequest(
      HttpServletRequest request) {
 
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        String domain = obtainDomain(request);

        // ...

        String usernameDomain = String.format("%s%s%s", username.trim(), 
          String.valueOf(Character.LINE_SEPARATOR), domain);
        return new UsernamePasswordAuthenticationToken(
          usernameDomain, password);
    }

    // other methods
}

3.2. Простой сервис UserDetails

Контракт UserDetailsService определяет один метод с именем loadUserByUsername. Наша реализация извлекает имя пользователя и домен. Затем значения передаются в наш репозиторий U ser для получения Пользователя :

public class SimpleUserDetailsService implements UserDetailsService {

    // ...

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String[] usernameAndDomain = StringUtils.split(
          username, String.valueOf(Character.LINE_SEPARATOR));
        if (usernameAndDomain == null || usernameAndDomain.length != 2) {
            throw new UsernameNotFoundException("Username and domain must be provided");
        }
        User user = userRepository.findUser(usernameAndDomain[0], usernameAndDomain[1]);
        if (user == null) {
            throw new UsernameNotFoundException(
              String.format("Username not found for domain, username=%s, domain=%s", 
                usernameAndDomain[0], usernameAndDomain[1]));
        }
        return user;
    }
}

3.3. Конфигурация безопасности Пружины

Наша настройка отличается от стандартной конфигурации безопасности Spring, потому что мы вставляем наш SimpleAuthenticationFilter в цепочку фильтров перед настройкой по умолчанию с вызовом addFilterBefore :

@Override
protected void configure(HttpSecurity http) throws Exception {

    http
      .addFilterBefore(authenticationFilter(), 
        UsernamePasswordAuthenticationFilter.class)
      .authorizeRequests()
        .antMatchers("/css/**", "/index").permitAll()
        .antMatchers("/user/**").authenticated()
      .and()
      .formLogin().loginPage("/login")
      .and()
      .logout()
      .logoutUrl("/logout");
}

Мы можем использовать предоставленный DaoAuthenticationProvider , потому что мы настраиваем его с помощью нашего SimpleUserDetailsService . Напомним, что наш Простой UserDetailsService знает, как разобрать ваши имя пользователя и домен поля и вернуть соответствующий Пользователь для использования при аутентификации:

public AuthenticationProvider authProvider() {
    DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
    provider.setUserDetailsService(userDetailsService);
    provider.setPasswordEncoder(passwordEncoder());
    return provider;
}

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

public SimpleAuthenticationFilter authenticationFilter() throws Exception {
    SimpleAuthenticationFilter filter = new SimpleAuthenticationFilter();
    filter.setAuthenticationManager(authenticationManagerBean());
    filter.setAuthenticationFailureHandler(failureHandler());
    return filter;
}

3.4. Страница Входа в Систему

Страница входа, которую мы используем, собирает наше дополнительное поле domain , которое извлекается нашим фильтром Simple Authentication:

Когда мы запускаем приложение и получаем доступ к контексту в http://localhost:8081 , мы видим ссылку для доступа к защищенной странице. При нажатии на ссылку откроется страница входа в систему. Как и ожидалось, мы видим дополнительное поле домена :

3.5. Резюме

В нашем первом примере мы смогли повторно использовать DaoAuthenticationProvider и UsernamePasswordAuthenticationToken , “подделав” поле username.

В результате мы смогли добавить поддержку дополнительного поля входа с минимальным количеством настроек и дополнительного кода .

4. Настройка Пользовательского Проекта

Наш второй подход будет очень похож на первый, но может быть более подходящим для нетривиальных случаев использования.

Ключевые компоненты нашего второго подхода будут включать:

  • CustomAuthenticationFilter расширение UsernamePasswordAuthenticationFilter
  • CustomUserDetailsService пользовательский интерфейс, объявляющий loadUserbyUsernameAndDomain метод
  • CustomUserDetailsServiceImpl реализация нашего CustomUserDetailsService
  • CustomUserDetailsAuthenticationProvider расширение AbstractUserDetailsAuthenticationProvider
  • CustomAuthenticationToken расширение UsernamePasswordAuthenticationToken
  • Us er расширение класса User , предоставляемого Spring Security, которое объявляет наше дополнительное доменное поле
  • Securi tyConfig наша весенняя конфигурация безопасности, которая вставляет наш CustomAuthenticationFilter в цепочку фильтров, объявляет правила безопасности и подключает зависимости
  • login.html |/– страница входа в систему, которая собирает имя пользователя , пароль и домен

4.1. Пользовательский Фильтр Аутентификации

В нашем CustomAuthenticationFilter мы извлекаем поля имени пользователя, пароля и домена из запроса . Эти значения используются для создания экземпляра нашего пользовательского токена аутентификации , который передается в AuthenticationProvider для аутентификации:

public class CustomAuthenticationFilter 
  extends UsernamePasswordAuthenticationFilter {

    public static final String SPRING_SECURITY_FORM_DOMAIN_KEY = "domain";

    @Override
    public Authentication attemptAuthentication(
        HttpServletRequest request,
        HttpServletResponse response) 
          throws AuthenticationException {

        // ...

        CustomAuthenticationToken authRequest = getAuthRequest(request);
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    private CustomAuthenticationToken getAuthRequest(HttpServletRequest request) {
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        String domain = obtainDomain(request);

        // ...

        return new CustomAuthenticationToken(username, password, domain);
    }

4.2. Пользовательский сервис UserDetailsService

Наш Custom UserDetailsService контракт определяет один метод с именем loadUserByUsername И Доменом.

Класс Custom UserDetailsServiceImpl , который мы создаем, просто реализует контракт и делегирует его нашему CustomUserRepository , чтобы получить User :

 public UserDetails loadUserByUsernameAndDomain(String username, String domain) 
     throws UsernameNotFoundException {
     if (StringUtils.isAnyBlank(username, domain)) {
         throw new UsernameNotFoundException("Username and domain must be provided");
     }
     User user = userRepository.findUser(username, domain);
     if (user == null) {
         throw new UsernameNotFoundException(
           String.format("Username not found for domain, username=%s, domain=%s", 
             username, domain));
     }
     return user;
 }

4.3. Пользовательский UserDetailsAuthenticationProvider

Наш Пользовательский поставщик аутентификации UserDetails/| расширяет AbstractUserDetailsAuthenticationProvider и делегирует наш CustomUserDetailService для получения Пользователя . Наиболее важной особенностью этого класса является реализация метода RetrieveUser .

Обратите внимание, что мы должны привести токен аутентификации к нашему Пользовательскому токену аутентификации для доступа к нашему пользовательскому полю:

@Override
protected UserDetails retrieveUser(String username, 
  UsernamePasswordAuthenticationToken authentication) 
    throws AuthenticationException {
 
    CustomAuthenticationToken auth = (CustomAuthenticationToken) authentication;
    UserDetails loadedUser;

    try {
        loadedUser = this.userDetailsService
          .loadUserByUsernameAndDomain(auth.getPrincipal()
            .toString(), auth.getDomain());
    } catch (UsernameNotFoundException notFound) {
 
        if (authentication.getCredentials() != null) {
            String presentedPassword = authentication.getCredentials()
              .toString();
            passwordEncoder.matches(presentedPassword, userNotFoundEncodedPassword);
        }
        throw notFound;
    } catch (Exception repositoryProblem) {
 
        throw new InternalAuthenticationServiceException(
          repositoryProblem.getMessage(), repositoryProblem);
    }

    // ...

    return loadedUser;
}

4.4. Резюме

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

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

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

  • В нашем простом подходе мы минимизировали объем кода, который нам нужно было написать. Мы смогли повторно использовать DaoAuthenticationProvider и Аутентификацию пароля пользователя, адаптировав имя пользователя с помощью пользовательской логики синтаксического анализа
  • В нашем более индивидуальном подходе мы обеспечили поддержку пользовательских полей, расширив AbstractUserDetailsAuthenticationProvider и предоставив наш собственный CustomUserDetailsService с помощью CustomAuthenticationToken

Как всегда, весь исходный код можно найти на GitHub .