Campos de inicio de sesión adicionales con Spring Security

1. Introducción

En este artículo, implementaremos un escenario de autenticación personalizado con Spring Security agregando un campo adicional al formulario de inicio de sesión estándar .

Nos centraremos en 2 enfoques diferentes para mostrar la versatilidad del marco y las formas flexibles en las que podemos usarlo.

Nuestro primer enfoque será una solución simple que se centra en la reutilización de las implementaciones centrales de Spring Security existentes.

Nuestro segundo enfoque será una solución más personalizada que puede ser más adecuada para casos de uso avanzados.

Nos basaremos en conceptos que se analizan en nuestros artículos anteriores sobre el inicio de sesión de Spring Security.

2. Configuración de Maven

Usaremos arrancadores Spring Boot para arrancar nuestro proyecto y traer todas las dependencias necesarias.

La configuración que usaremos requiere una declaración principal, un iniciador web y un iniciador de seguridad; también incluiremos hojas de tomillo:

 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 versión más actual del iniciador de seguridad Spring Boot se puede encontrar en Maven Central.

3. Configuración simple del proyecto

En nuestro primer enfoque, nos centraremos en reutilizar las implementaciones que proporciona Spring Security. En particular, reutilizaremos DaoAuthenticationProvider y UsernamePasswordToken ya que existen "fuera de la caja".

Los componentes clave incluirán:

  • SimpleAuthenticationFilter : una extensión de UsernamePasswordAuthenticationFilter
  • SimpleUserDetailsService : una implementación de UserDetailsService
  • Us er : una extensión de laclase User proporcionada por Spring Security que declara nuestrocampo de dominio adicional
  • Securi tyConfig : nuestra configuración de Spring Security que inserta nuestro SimpleAuthenticationFilter en la cadena de filtros, declara las reglas de seguridad y conecta las dependencias
  • login.html : una página de inicio de sesión que recopila el nombre de usuario , la contraseña y el dominio

3.1. Filtro de autenticación simple

En nuestro SimpleAuthenticationFilter , los campos de dominio y nombre de usuario se extraen de la solicitud . Concatenamos estos valores y los usamos para crear una instancia de UsernamePasswordAuthenticationToken .

Luego, el token se pasa al AuthenticationProvider para la autenticación :

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. Servicio Simple UserDetails

El contrato UserDetailsService define un método único llamado loadUserByUsername. Nuestra implementación extrae el nombre de usuario y el dominio. Luego, los valores se pasan a nuestro U serRepository para obtener el usuario :

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. Configuración de seguridad de primavera

Nuestra configuración es diferente de una configuración estándar de Spring Security porque insertamos nuestro SimpleAuthenticationFilter en la cadena de filtros antes del predeterminado con una llamada a 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"); }

Podemos usar el DaoAuthenticationProvider proporcionado porque lo configuramos con nuestro SimpleUserDetailsService . Recuerde que nuestro SimpleUserDetailsService sabe cómo analizar nuestros campos de nombre de usuario y dominio y devolver el Usuario apropiado para usar al autenticarse:

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

Dado que estamos usando un SimpleAuthenticationFilter , configuramos nuestro propio AuthenticationFailureHandler para garantizar que los intentos de inicio de sesión fallidos se manejen de manera adecuada:

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

3.4. Página de inicio de sesión

La página de inicio de sesión que utilizamos recopila nuestro campo de dominio adicional que se extrae mediante nuestro SimpleAuthenticationFilter:

Please sign in

Example: user / domain / password

Invalid user, password, or domain

Username

Domain

Password

Sign in

Back to home page

Cuando ejecutamos la aplicación y accedemos al contexto en // localhost: 8081, vemos un enlace para acceder a una página segura. Al hacer clic en el enlace, se mostrará la página de inicio de sesión. Como era de esperar, vemos el campo de dominio adicional :

3.5. Resumen

En nuestro primer ejemplo, pudimos reutilizar DaoAuthenticationProvider y UsernamePasswordAuthenticationToken "falsificando" el campo de nombre de usuario.

Como resultado, pudimos agregar soporte para un campo de inicio de sesión adicional con una cantidad mínima de configuración y código adicional .

4. Configuración de proyecto personalizado

Nuestro segundo enfoque será muy similar al primero, pero puede ser más apropiado para casos de usos no triviales.

Los componentes clave de nuestro segundo enfoque incluirán:

  • CustomAuthenticationFilter : una extensión de UsernamePasswordAuthenticationFilter
  • CustomUserDetailsService : una interfaz personalizada que declara unmétodo loadUserbyUsernameAndDomain
  • CustomUserDetailsServiceImpl : una implementación de nuestro CustomUserDetailsService
  • CustomUserDetailsAuthenticationProvider : una extensión de AbstractUserDetailsAuthenticationProvider
  • CustomAuthenticationToken : una extensión de UsernamePasswordAuthenticationToken
  • Us er : una extensión de laclase User proporcionada por Spring Security que declara nuestrocampo de dominio adicional
  • Securi tyConfig : nuestra configuración de Spring Security que inserta nuestro CustomAuthenticationFilter en la cadena de filtros, declara reglas de seguridad y conecta las dependencias
  • login.html : la página de inicio de sesión que recopila el nombre de usuario , la contraseña y el dominio

4.1. Filtro de autenticación personalizado

En nuestro CustomAuthenticationFilter , extraemos los campos de nombre de usuario, contraseña y dominio de la solicitud . Estos valores se utilizan para crear una instancia de nuestro AuthenticationToken personalizado que se pasa al AuthenticationProvider para la autenticación:

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. Servicio de detalles de usuario personalizados

Nuestro contrato CustomUserDetailsService define un método único llamado loadUserByUsernameAndDomain.

La clase CustomUserDetailsServiceImpl que creamos simplemente implementa el contrato y delega a nuestro CustomUserRepository para obtener el usuario :

 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
  • En nuestro enfoque más personalizado, proporcionamos soporte de campo personalizado al extender AbstractUserDetailsAuthenticationProvider y proporcionar nuestro propio CustomUserDetailsService con un CustomAuthenticationToken

Como siempre, todo el código fuente se puede encontrar en GitHub.