Champs de connexion supplémentaires avec sécurité Spring

Champs de connexion supplémentaires avec Spring Security

1. introduction

Dans cet article, nous allons mettre en œuvre un scénario d'authentification personnalisé avecSpring Security paradding an extra field to the standard login form.

Nous allons nous concentrer sur2 different approaches, pour montrer la polyvalence du framework et les manières flexibles de l'utiliser.

Our first approach sera une solution simple qui se concentre sur la réutilisation des implémentations de base existantes de Spring Security.

Our second approach sera une solution plus personnalisée qui peut être plus adaptée aux cas d'utilisation avancés.

Nous nous baserons sur les concepts abordés dans nosprevious articles on Spring Security login.

2. Maven Setup

Nous utiliserons les démarreurs Spring Boot pour démarrer notre projet et intégrer toutes les dépendances nécessaires.

La configuration que nous utiliserons nécessite une déclaration parent, un démarreur Web et un démarreur de sécurité. nous inclurons également thymeleaf:


    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
    

La version la plus récente du démarreur de sécurité Spring Boot peut être trouvéeover at Maven Central.

3. Configuration de projet simple

Dans notre première approche, nous nous concentrerons sur la réutilisation des implémentations fournies par Spring Security. En particulier, nous réutiliseronsDaoAuthenticationProvider etUsernamePasswordToken tels qu’ils existent «prêts à l’emploi».

Les composants clés comprendront:

  • SimpleAuthenticationFilter une extension deUsernamePasswordAuthenticationFilter

  • SimpleUserDetailsService une implémentation deUserDetailsService

  • User une extension de la classeUser fournie par Spring Security qui déclare notre champdomain supplémentaire

  • SecurityConfig notre configuration Spring Security qui insère nosSimpleAuthenticationFilter dans la chaîne de filtres, déclare les règles de sécurité et connecte les dépendances

  • login.html une page de connexion qui collecte lesusername,password etdomain

3.1. Filtre d'authentification simple

Dans nosSimpleAuthenticationFilter,the domain and username fields are extracted from the request. Nous concaténons ces valeurs et les utilisons pour créer une instance deUsernamePasswordAuthenticationToken.

The token is then passed along to the AuthenticationProvider pour l'authentification:

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. ServiceUserDetails simple

Le contratUserDetailsService définit une seule méthode appeléeloadUserByUsername.Our implementation extracts the username and domain. Les valeurs sont ensuite passées à nos UserRepository pour obtenir lesUser:

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. Configuration de sécurité de printemps

Notre configuration est différente d'une configuration standard de Spring Security carwe insert our SimpleAuthenticationFilter into the filter chain before the default avec un appel à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");
}

Nous pouvons utiliser lesDaoAuthenticationProvider fournis car nous les configurons avec nosSimpleUserDetailsService. Rappelez-vous queour SimpleUserDetailsService knows how to parse out our username and domain fields et renvoyez lesUser appropriés à utiliser lors de l'authentification:

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

Puisque nous utilisons unSimpleAuthenticationFilter, nous configurons nos propresAuthenticationFailureHandler pour nous assurer que les tentatives de connexion infructueuses sont gérées de manière appropriée:

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

3.4. Page de connexion

La page de connexion que nous utilisons recueille notre champdomain supplémentaire qui est extrait par nosSimpleAuthenticationFilter:

Lorsque nous exécutons l'application et accédons au contexte àhttp://localhost:8081, nous voyons un lien pour accéder à une page sécurisée. Cliquez sur le lien pour afficher la page de connexion. Comme prévu,we see the additional domain field:

image

3.5. Sommaire

Dans notre premier exemple, nous avons pu réutiliserDaoAuthenticationProvider etUsernamePasswordAuthenticationToken en «simulant» le champ du nom d'utilisateur.

En conséquence, nous avons puadd support for an extra login field with a minimal amount of configuration and additional code.

4. Configuration de projet personnalisée

Notre seconde approche sera très similaire à la première mais peut être plus appropriée pour des cas d'utilisation non triviaux.

Les éléments clés de notre deuxième approche comprendront:

  • CustomAuthenticationFilter une extension deUsernamePasswordAuthenticationFilter

  • CustomUserDetailsService une interface personnalisée déclarant une méthodeloadUserbyUsernameAndDomain

  • CustomUserDetailsServiceImpl une implémentation de nosCustomUserDetailsService

  • CustomUserDetailsAuthenticationProvider une extension deAbstractUserDetailsAuthenticationProvider

  • CustomAuthenticationToken une extension deUsernamePasswordAuthenticationToken

  • User une extension de la classeUser fournie par Spring Security qui déclare notre champdomain supplémentaire

  • SecurityConfig notre configuration Spring Security qui insère nosCustomAuthenticationFilter dans la chaîne de filtres, déclare les règles de sécurité et connecte les dépendances

  • login.html la page de connexion qui collecte lesusername,password etdomain

4.1. Filtre d'authentification personnalisé

Dans nosCustomAuthenticationFilter, nousextract the username, password, and domain fields from the request. Ces valeurs sont utilisées pour créer une instance de nosAuthenticationToken personnalisés qui est passée auxAuthenticationProvider pour l'authentification:

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. Service personnaliséUserDetails

Notre contratCustomUserDetailsService définit une seule méthode appeléeloadUserByUsernameAndDomain.

La classeCustomUserDetailsServiceImpl que nous créons implémente simplement le contrat et délègue à nosCustomUserRepository pour obtenir lesUser:

 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. PersonnaliséUserDetailsAuthenticationProvider

NotreCustomUserDetailsAuthenticationProvider étendAbstractUserDetailsAuthenticationProvider et les délègue à nosCustomUserDetailService pour récupérer lesUser. The most important feature of this class is the implementation of the retrieveUser method.

Notez que nous devons convertir le jeton d'authentification à nosCustomAuthenticationToken pour accéder à notre champ personnalisé:

@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. Sommaire

Notre deuxième approche est presque identique à l'approche simple que nous avons présentée en premier. En implémentant nos propresAuthenticationProvider etCustomAuthenticationToken, nous avons évité d'avoir à adapter notre champ de nom d'utilisateur avec une logique d'analyse personnalisée.

5. Conclusion

Dans cet article, nous avons implémenté un formulaire de connexion dans Spring Security qui utilisait un champ de connexion supplémentaire. Nous l'avons fait de 2 manières différentes:

  • Dans notre approche simple, nous avons minimisé la quantité de code nécessaire à l’écriture. Nous avons pureuse DaoAuthenticationProvider and UsernamePasswordAuthentication by adapting the username avec une logique d'analyse personnalisée

  • Dans notre approche plus personnalisée, nous avons fourni un support de terrain personnalisé parextending AbstractUserDetailsAuthenticationProvider and providing our own CustomUserDetailsService with a CustomAuthenticationToken

Comme toujours, tout le code source peut être trouvéover on GitHub.