Campos extras de login com Spring Security

Campos extras de login com Spring Security

1. Introdução

Neste artigo, implementaremos um cenário de autenticação personalizado comSpring Security poradding an extra field to the standard login form.

Vamos nos concentrar em2 different approaches, para mostrar a versatilidade da estrutura e as formas flexíveis em que podemos usá-la.

Our first approach será uma solução simples que se concentra na reutilização de implementações centrais existentes do Spring Security.

Our second approach será uma solução mais personalizada que pode ser mais adequada para casos de uso avançados.

Vamos construir sobre os conceitos que são discutidos em nossoprevious articles on Spring Security login.

2. Configuração do Maven

Usaremos iniciadores Spring Boot para inicializar nosso projeto e trazer todas as dependências necessárias.

A configuração que usaremos requer uma declaração pai, web starter e security starter; também incluiremos a folha timbrado:


    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
    

A versão mais atual do Spring Boot Security Starter pode ser encontradaover at Maven Central.

3. Configuração de projeto simples

Em nossa primeira abordagem, vamos nos concentrar em reutilizar as implementações fornecidas pelo Spring Security. Em particular, vamos reutilizarDaoAuthenticationProvider eUsernamePasswordToken, pois eles existem "fora da caixa".

Os principais componentes incluirão:

  • SimpleAuthenticationFilter uma extensão deUsernamePasswordAuthenticationFilter

  • SimpleUserDetailsService uma implementação deUserDetailsService

  • User uma extensão da classeUser fornecida pelo Spring Security que declara nosso campodomain extra

  • SecurityConfig nossa configuração Spring Security que insere nossoSimpleAuthenticationFilter na cadeia de filtros, declara regras de segurança e conecta dependências

  • login.html uma página de login que coletausername,password edomain

3.1. Filtro de autenticação simples

Em nossoSimpleAuthenticationFilter,the domain and username fields are extracted from the request. Concatenamos esses valores e os usamos para criar uma instância deUsernamePasswordAuthenticationToken.

The token is then passed along to the AuthenticationProvider para autenticação:

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. ServiçoUserDetails simples

O contratoUserDetailsService define um único método chamadoloadUserByUsername.Our implementation extracts the username and domain. Os valores são então passados ​​para o nosso UserRepository para obter oUser:

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. Configuração de segurança da primavera

Nossa configuração é diferente de uma configuração padrão do Spring Security porquewe insert our SimpleAuthenticationFilter into the filter chain before the default com uma chamada paraaddFilterBefore:

@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");
}

Podemos usar oDaoAuthenticationProvider fornecido porque o configuramos com nossoSimpleUserDetailsService. Lembre-se de queour SimpleUserDetailsService knows how to parse out our username and domain fieldse retorne oUser apropriado para usar na autenticação:

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

Como estamos usando umSimpleAuthenticationFilter, configuramos nosso próprioAuthenticationFailureHandler para garantir que as tentativas de login malsucedidas sejam tratadas de maneira adequada:

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

3.4. Página de login

A página de login que usamos coleta nosso campodomain adicional que é extraído por nossoSimpleAuthenticationFilter:

Quando executamos o aplicativo e acessamos o contexto emhttp://localhost:8081, vemos um link para acessar uma página segura. Clicar no link fará com que a página de login seja exibida. Como esperado,we see the additional domain field:

image

3.5. Sumário

Em nosso primeiro exemplo, fomos capazes de reutilizarDaoAuthenticationProvidereUsernamePasswordAuthenticationToken “falsificando” o campo de nome de usuário.

Como resultado, conseguimosadd support for an extra login field with a minimal amount of configuration and additional code.

4. Configuração de projeto personalizado

Nossa segunda abordagem será muito semelhante à primeira, mas pode ser mais apropriada para casos de uso não triviais.

Os principais componentes de nossa segunda abordagem incluirão:

  • CustomAuthenticationFilter uma extensão deUsernamePasswordAuthenticationFilter

  • CustomUserDetailsService uma interface personalizada que declara um métodoloadUserbyUsernameAndDomain

  • CustomUserDetailsServiceImpl uma implementação de nossoCustomUserDetailsService

  • CustomUserDetailsAuthenticationProvider uma extensão deAbstractUserDetailsAuthenticationProvider

  • CustomAuthenticationToken uma extensão deUsernamePasswordAuthenticationToken

  • User uma extensão da classeUser fornecida pelo Spring Security que declara nosso campodomain extra

  • SecurityConfig nossa configuração Spring Security que insere nossoCustomAuthenticationFilter na cadeia de filtros, declara regras de segurança e conecta dependências

  • login.html a página de login que coletausername,password edomain

4.1. Filtro de autenticação personalizado

Em nossoCustomAuthenticationFilter, nósextract the username, password, and domain fields from the request. Esses valores são usados ​​para criar uma instância de nossoAuthenticationToken personalizado que é passado paraAuthenticationProvider para autenticação:

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. ServiçoUserDetails personalizado

Nosso contratoCustomUserDetailsService define um único método chamadoloadUserByUsernameAndDomain.

A classeCustomUserDetailsServiceImpl que criamos simplesmente implementa o contrato e delega ao nossoCustomUserRepository para obter oUser:

 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 personalizado

NossoCustomUserDetailsAuthenticationProvider estendeAbstractUserDetailsAuthenticationProvider e delega ao nossoCustomUserDetailService para recuperar oUser. The most important feature of this class is the implementation of the retrieveUser method.

Observe que devemos lançar o token de autenticação em nossoCustomAuthenticationToken para acessar nosso campo personalizado:

@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. Sumário

Nossa segunda abordagem é quase idêntica à abordagem simples que apresentamos primeiro. Ao implementar nossos própriosAuthenticationProvidereCustomAuthenticationToken, evitamos a necessidade de adaptar nosso campo de nome de usuário com lógica de análise personalizada.

5. Conclusão

Neste artigo, implementamos um formulário de login no Spring Security que faz uso de um campo de login extra. Fizemos isso de 2 maneiras diferentes:

  • Em nossa abordagem simples, minimizamos a quantidade de código que precisávamos escrever. Conseguimosreuse DaoAuthenticationProvider and UsernamePasswordAuthentication by adapting the username com lógica de análise personalizada

  • Em nossa abordagem mais personalizada, fornecemos suporte de campo personalizado porextending AbstractUserDetailsAuthenticationProvider and providing our own CustomUserDetailsService with a CustomAuthenticationToken

Como sempre, todo o código-fonte pode ser encontradoover on GitHub.