Validación de contraseña personalizada de Spring

Actualmente, las políticas de contraseñas son muy comunes y existen en la mayoría de las plataformas en línea. Si bien a ciertos usuarios realmente no les gustan, hay una razón por la que existen:...

Introducción

Actualmente, las políticas de contraseñas son muy comunes y existen en la mayoría de las plataformas en línea. Si bien a ciertos usuarios realmente no les gustan, hay una razón por la que existen: hacer que las contraseñas sean más seguras.

Seguramente ha tenido experiencia con aplicaciones que imponen ciertas reglas para su contraseña, como el número mínimo o máximo de caracteres permitidos, incluidos dígitos, letras mayúsculas, etc.

No importa cuán bueno sea el sistema de seguridad, si un usuario elige una contraseña débil como "contraseña", los datos confidenciales pueden quedar expuestos. Si bien algunos usuarios pueden irritarse con las políticas de contraseñas, mantienen seguros los datos de su usuario, ya que hace que los ataques sean mucho más ineficientes.

Para implementar esto en nuestras aplicaciones basadas en Spring, usaremos Pasaje, una biblioteca creada específicamente para este propósito que facilita la aplicación de políticas de contraseñas en Java.

Tenga en cuenta: este tutorial asume que tiene conocimientos básicos del marco Spring, por lo que nos centraremos más en Passay por brevedad.

Aside from password policies, a good and fundamental technique to implement for security is Codificación de contraseña.

Formulario de registro {#formulario de registro}

La forma más sencilla de comenzar con un proyecto básico de Spring Boot, como siempre, es usar Spring Initializr.

Seleccione su versión preferida de Spring Boot y agregue las dependencias Web y Thymeleaf:

spring_initializr

Después de esto, genere como un proyecto Maven y ¡ya está todo listo!

Definamos un Objeto de transferencia de datos simple en el que incluiremos todos los atributos que queremos capturar de nuestro formulario:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class UserDto {

    @NotEmpty
    private String name;

    @Email
    @NotEmpty
    private String email;

    private String password;

Todavía no hemos anotado el campo de contraseña, porque implementaremos una anotación personalizada para esto.

Luego tenemos una clase de controlador simple que sirve el formulario de registro y captura sus datos cuando se envían mediante las asignaciones GET/POST:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Controller
@RequestMapping("/signup")
public class SignUpController {

    @ModelAttribute("user")
    public UserDto userDto() {
        return new UserDto();
    }

    @GetMapping
    public String showForm() {
        return "signup";
    }

    @PostMapping
    public String submitForm(@Valid @ModelAttribute("user") UserDto user, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "signup";
        }
        return "success";
    }

}

Primero definimos un @ModelAttribute("usuario") y le asignamos una instancia UserDto. Este es el objeto que contendrá la información una vez enviada.

Usando este objeto, podemos extraer los datos y conservarlos en la base de datos.

El método showForm() devuelve un String con el valor "signup". Ya que tenemos Thymeleaf en nuestro classpath, Spring buscará "signup.html" en la carpeta de plantillas en recursos.

De manera similar, tenemos un mapeo submitForm() POST que verificará si el formulario tiene algún error. Si lo hace, lo redirigirá a la página "signup.html". De lo contrario, redirigirá al usuario a la página de "éxito".

hoja de tomillo es un moderno motor de plantillas Java del lado del servidor para procesar y crear HTML, XML, JavaScript, CSS y texto. Es una alternativa moderna para motores de plantillas más antiguos como Páginas del servidor Java (JSP).

Avancemos y definamos una página "signup.html":

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<form action="#" th:action="@{/signup}" th:object="${user}" method="post">

    <div class="form-group">
        <input type="text" th:field="*{name}" class="form-control"
               id="name" placeholder="Name"> <span
               th:if="${#fields.hasErrors('name')}" th:errors="*{name}"
               class="text-danger"></span>
     </div>
     <div class="form-group">
        <input type="text" th:field="*{email}" class="form-control"
               id="email" placeholder="Email"> <span
               th:if="${#fields.hasErrors('email')}" th:errors="*{email}"
               class="text-danger"></span>
     </div>
     <div class="form-group">
         <input type="text" th:field="*{password}" class="form-control"
                id="password" placeholder="Password">
         <ul class="text-danger" th:each="error: ${#fields.errors('password')}">
             <li th:each="message : ${error.split(',')}">
                 <p class="error-message" th:text="${message}"></p>
             </li>
         </ul>
     </div>

     <div class="col-md-6 mt-5">
         <input type="submit" class="btn btn-primary" value="Submit">
     </div>
</form>

Hay algunas cosas que deben señalarse aquí:

  • th:action = "@{/signup}" - El atributo de acción se refiere a la URL a la que llamamos al enviar el formulario. Estamos apuntando a la asignación de URL de "registro" en nuestro controlador.
  • method="post" - El atributo de método se refiere al tipo de solicitud que estamos enviando. Esto tiene que coincidir con el tipo de solicitud definida en el método submitForm().
  • th:object="${user}" - El atributo del objeto se refiere al nombre del objeto que hemos definido en el controlador anteriormente usando @ModelAttribute("user"). Usando el resto del formulario, completaremos los campos de la instancia UserDto y luego guardaremos la instancia.

Tenemos otros 3 campos de entrada que se asignan a nombre, correo electrónico y contraseña utilizando la etiqueta th:field. Si los campos tienen errores, el usuario será notificado a través de la etiqueta th:errors.

Ejecutemos nuestra aplicación y naveguemos a http://localhost:8080/registro:

spring_initializr

Custom @ValidPassword Anotación

Según los requisitos del proyecto, a veces tenemos que definir un código personalizado específico para nuestras aplicaciones.

Dado que podemos hacer cumplir diferentes políticas y reglas, avancemos y definamos una anotación personalizada que verifique una contraseña válida, que usaremos en nuestra clase UserDto.

Las anotaciones son solo metadatos para el código y no contienen ninguna lógica empresarial. Solo pueden proporcionar información sobre el atributo (clase/método/paquete/campo) en el que está definido.

Vamos a crear nuestra anotación @ValidPassword:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Documented
@Constraint(validatedBy = PasswordConstraintValidator.class)
@Target({ FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
public @interface ValidPassword {

    String message() default "Invalid Password";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

Como puede ver, para crear una anotación usamos la palabra clave @interface. Echemos un vistazo a algunas de las palabras clave y comprendamos completamente antes de continuar:

  • @Documentado: Un simple marcador de anotaciones que indica si se debe agregar una Anotación en Javadocs o no.
  • @Constraint: Marca una anotación como Restricción de validación de beans. El elemento validado por especifica las clases que implementan la restricción. Crearemos la clase PasswordConstraintValidator un poco más adelante.
  • @Target: Es donde se pueden usar nuestras anotaciones. Si no especifica esto, la anotación se puede colocar en cualquier lugar. Actualmente, nuestra anotación se puede colocar sobre una variable de instancia y sobre otras anotaciones.
  • @Retención: Define por cuánto tiempo se debe mantener la anotación. Hemos elegido RUNTIME para que pueda ser utilizado por el entorno de tiempo de ejecución.

Para usar esto en nuestra clase UserDto simplemente anote el campo de contraseña:

1
2
@ValidPassword
private String password;

Validador de restricción de contraseña personalizada

Ahora que tenemos nuestra anotación, implementemos la lógica de validación para ella. Antes de eso, asegúrese de tener la dependencia de Passay Maven incluida en su archivo pom.xml:

1
2
3
4
5
<dependency>
    <groupId>org.passay</groupId>
    <artifactId>passay</artifactId>
    <version>{$version}</version>
</dependency>

Puedes consultar la última dependencia aquí.

Finalmente, escribamos nuestra clase PasswordConstraintValidator:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class PasswordConstraintValidator implements ConstraintValidator<ValidPassword, String> {

    @Override
    public void initialize(ValidPassword arg0) {
    }

    @Override
    public boolean isValid(String password, ConstraintValidatorContext context) {
        PasswordValidator validator = new PasswordValidator(Arrays.asList(
            // at least 8 characters
            new LengthRule(8, 30),

            // at least one upper-case character
            new CharacterRule(EnglishCharacterData.UpperCase, 1),

            // at least one lower-case character
            new CharacterRule(EnglishCharacterData.LowerCase, 1),

            // at least one digit character
            new CharacterRule(EnglishCharacterData.Digit, 1),

            // at least one symbol (special character)
            new CharacterRule(EnglishCharacterData.Special, 1),

            // no whitespace
            new WhitespaceRule()

        ));
        RuleResult result = validator.validate(new PasswordData(password));
        if (result.isValid()) {
            return true;
        }
        List<String> messages = validator.getMessages(result);

        String messageTemplate = messages.stream()
            .collect(Collectors.joining(","));
        context.buildConstraintViolationWithTemplate(messageTemplate)
            .addConstraintViolation()
            .disableDefaultConstraintViolation();
        return false;
    }
}

Implementamos la interfaz ConstraintValidator que nos obliga a implementar un par de métodos.

Primero creamos un objeto PasswordValidator pasando una serie de restricciones que queremos aplicar en nuestra contraseña.

Las restricciones se explican por sí mismas:

  • Debe tener entre 8 y 30 caracteres, según lo define LengthRule
  • Debe tener al menos 1 carácter en minúscula según lo definido por CharacterRule
  • Debe tener al menos 1 carácter en mayúscula según lo definido por CharacterRule
  • Debe tener al menos 1 carácter especial según lo definido por CharacterRule
  • Debe tener al menos un carácter de 1 dígito según lo definido por CharacterRule
  • No debe contener espacios en blanco según lo definido por WhitespaceRule

La lista completa de reglas que se pueden escribir usando Passay se puede encontrar en la página web oficial.

Finalmente, validamos la contraseña y devolvemos true si pasa todas las condiciones. Si algunas condiciones fallan, agregamos todos los mensajes de error de la condición fallida en una cadena separada por "," y luego lo colocamos en el contexto y devolvemos falso.

Ejecutemos nuestro proyecto nuevamente e ingresemos una contraseña no válida para verificar que la validación funcione:

spring_initializr

Conclusión

En este artículo, cubrimos cómo hacer cumplir ciertas reglas de contraseña usando la biblioteca Passay. Creamos una anotación personalizada y un validador de restricción de contraseña para esto y lo usamos en nuestra variable de instancia, y la lógica comercial real se implementó en una clase separada.

Como siempre, el código de los ejemplos utilizados en este artículo se puede encontrar en Github.