Champs de connexion supplémentaires avec Spring Security

1. Introduction

Dans cet article, nous allons implémenter un scénario d'authentification personnalisé avec Spring Security en ajoutant un champ supplémentaire au formulaire de connexion standard .

Nous allons nous concentrer sur 2 approches différentes , pour montrer la polyvalence du framework et les manières flexibles de l'utiliser.

Notre première approche sera une solution simple qui se concentre sur la réutilisation des implémentations de base existantes de Spring Security.

Notre deuxième approche sera une solution plus personnalisée qui peut être plus adaptée aux cas d'utilisation avancés.

Nous allons construire sur les concepts abordés dans nos articles précédents sur la connexion Spring Security.

2. Configuration de Maven

Nous utiliserons les démarreurs Spring Boot pour amorcer notre projet et introduire 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.2.6.RELEASE     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  

La version la plus récente du démarreur de sécurité Spring Boot est disponible sur Maven Central.

3. Configuration simple du projet

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

Les éléments clés comprendront:

  • SimpleAuthenticationFilter - une extension de UsernamePasswordAuthenticationFilter
  • SimpleUserDetailsService - une implémentation de UserDetailsService
  • Us er - une extension de laclasse User fournie par Spring Security qui déclare notrechamp de domaine supplémentaire
  • Securi tyConfig - notre configuration Spring Security qui insère notre SimpleAuthenticationFilter dans la chaîne de filtrage, déclare les règles de sécurité et connecte les dépendances
  • login.html - une page de connexion qui recueille le nom d'utilisateur , le mot de passe et le domaine

3.1. Filtre d'authentification simple

Dans notre SimpleAuthenticationFilter , les champs domaine et nom d'utilisateur sont extraits de la demande . Nous concaténons ces valeurs et les utilisons pour créer une instance de UsernamePasswordAuthenticationToken .

Le jeton est ensuite transmis à 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. Service UserDetails simple

Le contrat UserDetailsService définit une méthode unique appelée loadUserByUsername. Notre implémentation extrait le nom d'utilisateur et le domaine. Les valeurs sont ensuite transmises à notre U serRepository pour obtenir l' utilisateur :

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 la sécurité Spring

Notre configuration est différente d'une configuration standard de Spring Security car nous insérons notre SimpleAuthenticationFilter dans la chaîne de filtres avant la valeur par défaut 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 sommes en mesure d'utiliser le DaoAuthenticationProvider fourni car nous le configurons avec notre SimpleUserDetailsService . Rappelez-vous que notre SimpleUserDetailsService sait comment analyser nos champs de nom d'utilisateur et de domaine et renvoyer l' utilisateur approprié à utiliser lors de l'authentification:

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

Puisque nous utilisons un SimpleAuthenticationFilter , nous configurons notre propre AuthenticationFailureHandler pour garantir 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 champ de domaine supplémentaire qui est extrait par notre SimpleAuthenticationFilter:

Please sign in

Example: user / domain / password

Invalid user, password, or domain

Username

Domain

Password

Sign in

Back to home page

Lorsque nous exécutons l'application et accédons au contexte à // 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, nous voyons le champ de domaine supplémentaire :

3.5. Sommaire

Dans notre premier exemple, nous avons pu réutiliser DaoAuthenticationProvider et UsernamePasswordAuthenticationToken en «simulant» le champ du nom d'utilisateur.

As a result, we were able to add support for an extra login field with a minimal amount of configuration and additional code.

4. Custom Project Setup

Our second approach will be very similar to the first but may be more appropriate for non-trivial uses cases.

The key components of our second approach will include:

  • CustomAuthenticationFilteran extension of UsernamePasswordAuthenticationFilter
  • CustomUserDetailsServicea custom interface declaring a loadUserbyUsernameAndDomain method
  • CustomUserDetailsServiceImplan implementation of our CustomUserDetailsService
  • CustomUserDetailsAuthenticationProvideran extension of AbstractUserDetailsAuthenticationProvider
  • CustomAuthenticationTokenan extension of UsernamePasswordAuthenticationToken
  • Useran extension of the User class provided by Spring Security that declares our extra domain field
  • SecurityConfigour Spring Security configuration that inserts our CustomAuthenticationFilter into the filter chain, declares security rules and wires up dependencies
  • login.htmlthe login page that collects the username, password, and domain

4.1. Custom Authentication Filter

In our CustomAuthenticationFilter, we extract the username, password, and domain fields from the request. These values are used to create an instance of our CustomAuthenticationToken which is passed to the AuthenticationProvider for authentication:

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. Custom UserDetails Service

Our CustomUserDetailsService contract defines a single method called loadUserByUsernameAndDomain.

The CustomUserDetailsServiceImpl class we create simply implements the contract and delegates to our CustomUserRepository to get the 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. Custom UserDetailsAuthenticationProvider

Our CustomUserDetailsAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider and delegates to our CustomUserDetailService to retrieve the User. The most important feature of this class is the implementation of the retrieveUser method.

Note that we must cast the authentication token to our CustomAuthenticationToken for access to our custom field:

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

Our second approach is nearly identical to the simple approach we presented first. By implementing our own AuthenticationProvider and CustomAuthenticationToken, we avoided needing to adapt our username field with custom parsing logic.

5. Conclusion

In this article, we've implemented a form login in Spring Security that made use of an extra login field. We did this in 2 different ways:

  • In our simple approach, we minimized the amount of code we needed write. We were able to reuse DaoAuthenticationProvider and UsernamePasswordAuthentication by adapting the username with custom parsing logic
  • Dans notre approche plus personnalisée, nous avons fourni une prise en charge de champ personnalisé en étendant AbstractUserDetailsAuthenticationProvider et en fournissant notre propre CustomUserDetailsService avec un CustomAuthenticationToken

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