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

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

1. Вступление

В этой статье мы реализуем сценарий пользовательской аутентификации сSpring Security поadding an extra field to the standard login form.

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

Our first approach будет простым решением, ориентированным на повторное использование существующих основных реализаций Spring Security.

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

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

2. Maven Setup

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

Для настройки, которую мы будем использовать, требуется родительская декларация, веб-стартер и стартер безопасности; мы также добавим тимелист:


    org.springframework.boot
    spring-boot-starter-parent
    2.0.0.M7
    



    
        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-springsecurity4
    

Самую последнюю версию стартера безопасности Spring Boot можно найти вover at Maven Central.

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

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

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

  • SimpleAuthenticationFilter расширениеUsernamePasswordAuthenticationFilter

  • SimpleUserDetailsService реализацияUserDetailsService

  • User расширение классаUser, предоставляемое Spring Security, которое объявляет наше дополнительное полеdomain

  • SecurityConfig наша конфигурация Spring Security, которая вставляет нашSimpleAuthenticationFilter в цепочку фильтров, объявляет правила безопасности и связывает зависимости

  • login.html страница входа, которая собираетusername,password иdomain

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

В нашемSimpleAuthenticationFilter,the domain and username fields are extracted from the request. Мы объединяем эти значения и используем их для создания экземпляраUsernamePasswordAuthenticationToken.

The token is then passed along to the AuthenticationProvider для аутентификации:

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.Our implementation extracts the username and domain.. Затем значения передаются в наш UserRepository, чтобы получитьUser:

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 Security

Наша установка отличается от стандартной конфигурации Spring Security, потому чтоwe insert our SimpleAuthenticationFilter into the filter chain before the default с вызовом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. Вспомните, чтоour SimpleUserDetailsService knows how to parse out our username and domain fields и верните соответствующийUser для использования при аутентификации:

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

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

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

3.4. Страница авторизации

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

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

image

3.5. Резюме

В нашем первом примере мы смогли повторно использоватьDaoAuthenticationProvider иUsernamePasswordAuthenticationToken, «подделав» поле имени пользователя.

В результате мы смоглиadd support for an extra login field with a minimal amount of configuration and additional code.

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

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

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

  • CustomAuthenticationFilter расширениеUsernamePasswordAuthenticationFilter

  • CustomUserDetailsService настраиваемый интерфейс, объявляющий методloadUserbyUsernameAndDomain

  • CustomUserDetailsServiceImpl реализация нашегоCustomUserDetailsService

  • CustomUserDetailsAuthenticationProvider расширениеAbstractUserDetailsAuthenticationProvider

  • CustomAuthenticationToken расширениеUsernamePasswordAuthenticationToken

  • User расширение классаUser, предоставляемое Spring Security, которое объявляет наше дополнительное полеdomain

  • SecurityConfig наша конфигурация Spring Security, которая вставляет нашCustomAuthenticationFilter в цепочку фильтров, объявляет правила безопасности и связывает зависимости

  • login.html страница входа, которая собираетusername,password иdomain

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

В нашемCustomAuthenticationFilter мыextract the username, password, and domain fields from the request. Эти значения используются для создания экземпляра нашего CustomAuthenticationToken, который передается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. Пользовательская услугаUserDetails

Наш контрактCustomUserDetailsService определяет единственный метод под названиемloadUserByUsernameAndDomain.

Созданный нами классCustomUserDetailsServiceImpl просто реализует контракт и делегирует нашему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

НашCustomUserDetailsAuthenticationProvider расширяетAbstractUserDetailsAuthenticationProvider и делегирует нашемуCustomUserDetailService для полученияUser. The most important feature of this class is the implementation of the retrieveUser method.

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

@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. Резюме

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

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

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

  • В нашем простом подходе мы минимизировали объем кода, который нам нужно было написать. Мы смоглиreuse DaoAuthenticationProvider and UsernamePasswordAuthentication by adapting the username с помощью настраиваемой логики синтаксического анализа

  • В нашем более индивидуальном подходе мы предоставили поддержку настраиваемых полейextending AbstractUserDetailsAuthenticationProvider and providing our own CustomUserDetailsService with a CustomAuthenticationToken

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