Skip to main content

Guía para validar entradas con Spring Boot

Escrito por
Headshot of Lucien Chemaly

Lucien Chemaly

feature bean validation

12 de septiembre de 2023

0 minutos de lectura
{"id":1,"name":"John Doe Updated","email":"john.doe.updated@example.com","password":"newP@ssW0rd!"}

Si desarrollas con Java, probablemente conozcas Spring Boot, el sólido framework que simplifica el desarrollo de aplicaciones independientes basadas en Spring y listas para producción. Una de las muchas funciones que ofrece es Bean Validation, un aspecto fundamental de cualquier aplicación para garantizar la integridad de los datos y mejorar la experiencia del usuario.

Bean Validation se puede usar en aplicaciones Spring Boot para validar entradas y formularios, validar datos antes de guardarlos en una base de datos y aplicar diversas políticas de seguridad. Con Bean Validation, los desarrolladores pueden prevenir errores, mejorar la calidad general de la aplicación y garantizar la coherencia de los datos en toda la aplicación.

En este artículo, aprenderás más sobre las distintas aplicaciones de Bean Validation, incluido cómo implementarlo en Spring Boot para aprovecharlo eficazmente en tus propios proyectos.

Qué es Bean Validation

Bean Validation es una función que permite aplicar restricciones a los modelos de datos de tu aplicación para garantizar que los datos cumplan reglas específicas antes de procesarlos o almacenarlos. Esto se logra mediante la integración de Jakarta Bean Validation, que proporciona un conjunto de anotaciones e interfaces para definir y validar restricciones.

Al aplicar Bean Validation, los desarrolladores pueden ahorrar tiempo y recursos al detectar errores e inconsistencias en las primeras etapas del proceso de desarrollo. Además, Bean Validation ayuda a mejorar la experiencia general del usuario al reducir la cantidad de datos incorrectos que se ingresan.

Veamos algunos casos de uso comunes de Bean Validation en el desarrollo de aplicaciones:

Validación de entradas y formularios

Uno de los casos de uso más comunes de Bean Validation es la validación de entradas y formularios. Esto ayuda a garantizar que los usuarios ingresen datos en el formato correcto (por ejemplo, validar las direcciones de correo electrónico, los nombres de usuario y las contraseñas en un formulario de registro).

Validación de datos antes de guardarlos en una base de datos

Otra aplicación de Bean Validation es validar los datos antes de guardarlos en una base de datos. Esta técnica ayuda a garantizar que los datos ingresados sean coherentes y precisos. A su vez, esto ayuda a prevenir errores y reduce el riesgo de corrupción de datos.

Aplicación de políticas de seguridad

Bean Validation también se puede usar para aplicar políticas de seguridad mediante la validación de las entradas de los usuarios o de los datos obtenidos de la base de datos, con el fin de garantizar que cumplan criterios específicos, como los requisitos de fortaleza de las contraseñas. La validación desempeña un papel clave en la prevención de ataques de inyección y ayuda a evitar vulnerabilidades de seguridad como el cross-site scripting (XSS) almacenado.

Validación de la lógica de negocio

Bean Validation se puede aplicar para validar la lógica de negocio y garantizar que cumpla requisitos o estándares específicos. Al definir restricciones de validación, como la complejidad de las contraseñas o las cantidades y los precios válidos de los artículos de un pedido, Bean Validation puede revisar los datos automáticamente y aplicar las reglas necesarias. Esto elimina la necesidad de revisiones manuales y garantiza que la aplicación cumpla estándares o requisitos específicos.

Implementación de Bean Validation en Spring Boot

En este tutorial, implementarás Bean Validation en una aplicación sencilla de crear, leer, actualizar y eliminar (CRUD) con una base de datos en memoria. Los usuarios proporcionan su nombre, correo electrónico y contraseña, que deben cumplir criterios específicos para que se acepten. La arquitectura de alto nivel que se usa aquí consta de un servidor backend con API RESTful y una base de datos H2 en memoria.

Cuando una aplicación cliente llama al servicio web (la aplicación de este tutorial), la solicitud llega a UserController. Luego, el controlador valida los datos de la solicitud. Si los datos son válidos, la solicitud continúa a UserRepository, que se comunica con la base de datos en memoria y devuelve la respuesta correspondiente. De lo contrario, los datos no son válidos y la solicitud se devuelve al cliente con un mensaje de error:

Diagrama de flujo que muestra una solicitud del cliente que pasa por la validación de beans de UserController hasta UserRepository, con rutas para casos válidos y errores de validación.

Requisitos previos

Antes de comenzar la implementación, asegúrate de tener instaladas las siguientes herramientas y tecnologías:

Crear un nuevo proyecto de Spring Boot

Para crear un proyecto Spring Boot, visita Spring Initializr y elige las siguientes opciones:

  • Tipo de proyecto: Maven Project

  • Lenguaje: Java

  • Empaquetado: Jar

  • Versión de Java: 17

  • Spring Boot: 3.1.0 (SNAPSHOT)

Ingresa los siguientes detalles en la sección Project Metadata:

  • Grupo: com.example

  • Artefacto: simple-crud-bean-validation

  • Nombre: simple-crud-bean-validation

  • Descripción: Una aplicación CRUD sencilla con Spring Boot

  • Nombre del paquete: com.example.simplecrud

Y agrega las siguientes dependencias:

  • Web: Spring Web

  • Validación: Bean Validation

  • Base de datos H2: H2 Database

  • Spring Data JPA: Spring Data e Hibernate

Configuración de Spring Initializr que muestra un proyecto CRUD de Java Spring Boot con las dependencias Bean Validation, H2 Database, Spring Data JPA y Spring Web

Haz clic en Generate para descargar el proyecto como archivo ZIP. Luego, extrae el archivo ZIP e importa el proyecto en el IDE que prefieras.

Implementar la aplicación

Seguiremos la estructura estándar de los proyectos Spring Boot. Esta es la estructura completa del proyecto:

Árbol de directorios del proyecto de una aplicación sencilla de validación de beans CRUD, que muestra archivos Java de código fuente, modelo, repositorio, controlador y pruebas, además de archivos Maven.

Modelo User

Para implementar la aplicación, primero debes crear el directorio model dentro de src/main/java/com/example/simplecrudbeanvalidation. Luego, dentro del directorio model, crea un archivo llamado User.java y agrega los siguientes campos y anotaciones de Bean Validation a la clase User:

import jakarta.persistence.*;
import jakarta.validation.constraints.*;

@Entity
@Table(name="users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull
    @NotEmpty
    @Pattern(regexp = "[a-zA-Z0-9 ]")
    private String name;

    @NotNull
    @NotEmpty
    @Email
    @Pattern(regexp=".+@.+\\..+")
    private String email;

    @NotNull
    @NotEmpty
    @Size(min = 8, max = 64)
    private String password;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public User(Long id, String name, String email, String password){
        this.id = id;
        this.name = name;
        this.email = email;
        this.password = password;
    }

    //default constructor
    public User(){}
}

Este código define una clase Java llamada User que representa una tabla de la base de datos llamada users, mediante anotaciones de Jakarta Persistence API (JPA). Tiene cuatro variables de instancia —id, name, email y password— con sus correspondientes getters y setters. Cada campo tiene ciertas restricciones de validación, como @NotNull, @NotEmpty, @Email, @Size y @Pattern. @Pattern se usa para indicar que la contraseña debe tener entre ocho y sesenta y cuatro caracteres e incluir al menos un dígito, una letra minúscula, una letra mayúscula y un carácter especial. Además, @Pattern también se usa en la propiedad name para prevenir la inyección de scripts. La clase se puede usar como modelo para almacenar y recuperar datos de usuarios en una base de datos.

Repositorio User

Para gestionar las operaciones CRUD con una base de datos en memoria, crea el directorio repository dentro de src/main/java/com/example/simplecrudbeanvalidation. Luego, dentro del directory repository, crea un archivo llamado UserRepository.java para definir la interfaz UserRepository, de la siguiente manera:

import com.example.simplecrudbeanvalidation.model.User;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends CrudRepository<User, Long> {
}

Este código define una interfaz Java llamada UserRepository que extiende la interfaz CrudRepository proporcionada por Spring Data JPA. La interfaz UserRepository especifica dos parámetros genéricos para CrudRepository: User, que es el tipo de entidad que administra este repositorio, y Long, que es el tipo de clave principal de la entidad.

Al extender CrudRepository, UserRepository hereda varios métodos para realizar operaciones CRUD en la entidad User. Estos métodos se pueden usar para interactuar con una base de datos en memoria y administrar objetos User sin necesidad de una configuración compleja ni de un servidor de base de datos completo.

Controlador User

Para crear la clase UserController, que define los endpoints de la API RESTful y la lógica de validación, primero crea un directorio llamado controller dentro del directorio src/main/java/com/example/simplecrudbeanvalidation. Dentro del directorio controller, crea un archivo llamado UserController.java con el siguiente contenido:

import com.example.simplecrudbeanvalidation.model.User;
import com.example.simplecrudbeanvalidation.repository.UserRepository;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/api/users")
@Validated
public class UserController
{
   @Autowired
   private UserRepository userRepository;

   @GetMapping
   public List<User> getAllUsers() {
       return (List<User>) userRepository.findAll();
   }

   @GetMapping("/{id}")
   public ResponseEntity<User> getUserById(@PathVariable Long id) {
       Optional<User> userOptional = userRepository.findById(id);
       if (userOptional.isPresent()) {
           return ResponseEntity.ok(userOptional.get());
       } else {
           return ResponseEntity.notFound().build();
       }
   }

   @PostMapping
   public User createUser(@Valid @RequestBody User user) {
       return userRepository.save(user);
   }

   @PutMapping("/{id}")
   public ResponseEntity<User> updateUser(@PathVariable Long id, @Valid @RequestBody User updatedUser) {
       Optional<User> userOptional = userRepository.findById(id);
       if (userOptional.isPresent()) {
           User user = userOptional.get();
           user.setName(updatedUser.getName());
           user.setEmail(updatedUser.getEmail());
           user.setPassword(updatedUser.getPassword());
           userRepository.save(user);
           return ResponseEntity.ok(user);
       } else {
           return ResponseEntity.notFound().build();
       }
   }

   @DeleteMapping("/{id}")
   public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
       if (userRepository.existsById(id)) {
           userRepository.deleteById(id);
           return ResponseEntity.ok().build();
       } else {
           return ResponseEntity.notFound().build();
       }
   }
}

Este código define una clase RestController de Spring Boot llamada UserController que gestiona las solicitudes HTTP para administrar objetos User. La clase contiene cinco métodos para gestionar distintas solicitudes HTTP: obtener todos los usuarios, obtener un usuario por ID, crear un usuario, actualizar uno existente y eliminar un usuario.

La anotación @Autowired se usa para inyectar la dependencia UserRepository en la clase UserController. La clase UserController se asocia con el endpoint /api/users mediante la anotación @RequestMapping, que asigna las solicitudes HTTP entrantes a este controlador. Además, la anotación @Validated permite validar los parámetros de las solicitudes. En conjunto, este código proporciona una API RESTful para operaciones CRUD en objetos User mediante Spring Data y una base de datos en memoria.

Configurar la base de datos en memoria

Para configurar la base de datos en memoria, abre application.properties en src/main/resources/ y agrega las siguientes configuraciones:

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

Esta configuración de Spring Boot establece una base de datos H2 en memoria llamada testdb y configura su controlador, nombre de usuario y contraseña. También especifica el dialecto H2 que debe usarse para las operaciones de base de datos basadas en Hibernate.

No se recomienda usar H2 en producción

No se recomienda usar H2 como base de datos en producción, ni usar el nombre de usuario y la contraseña predeterminados en un entorno de producción. Aunque esta configuración es adecuada para demostraciones, no debe usarse en producción.

Prueba unitaria de Bean Validation

Una vez que hayas configurado la base de datos en memoria, puedes empezar a realizar pruebas unitarias de Bean Validation. Para ello, crea el directorio controller dentro de src/test/java/com/example/simplecrudbeanvalidation. Luego, dentro del directorio controller, crea un archivo llamado UserControllerTest.java y escribe pruebas para verificar que las restricciones de Bean Validation funcionen según lo esperado.

En la clase UserControllerTest, agrega el siguiente código:

import com.example.simplecrudbeanvalidation.model.User;
import com.example.simplecrudbeanvalidation.repository.UserRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import java.util.Arrays;
import java.util.Optional;
import static org.hamcrest.Matchers.hasSize;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserRepository userRepository;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    public void createUser_validData_success() throws Exception {
        User newUser = new User(null, "John Doe", "john.doe@example.com", "p@ssW0rd!");
        User savedUser = new User(1L, newUser.getName(), newUser.getEmail(), newUser.getPassword());

        when(userRepository.save(any(User.class))).thenReturn(savedUser);

        mockMvc.perform(post("/api/users")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(newUser)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(savedUser.getId()))
                .andExpect(jsonPath("$.name").value(savedUser.getName()))
                .andExpect(jsonPath("$.email").value(savedUser.getEmail()))
                .andExpect(jsonPath("$.password").value(savedUser.getPassword()));
    }

    @Test
    public void createUser_invalidData_failure() throws Exception {
        User invalidUser = new User(null, "", "invalid-email", "123");

        mockMvc.perform(post("/api/users")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(invalidUser)))
                .andExpect(status().isBadRequest());
    }

    @Test
    public void getAllUsers() throws Exception {
        User user1 = new User(1L, "John Doe", "john.doe@example.com", "p@ssW0rd!");
        User user2 = new User(2L, "Jane Doe", "jane.doe@example.com", "p@ssW0rd!");
        when(userRepository.findAll()).thenReturn(Arrays.asList(user1, user2));
        mockMvc.perform(get("/api/users"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$", hasSize(2)))
                .andExpect(jsonPath("$[0].id").value(user1.getId()))
                .andExpect(jsonPath("$[0].name").value(user1.getName()))
                .andExpect(jsonPath("$[0].email").value(user1.getEmail()))
                .andExpect(jsonPath("$[0].password").value(user1.getPassword()))
                .andExpect(jsonPath("$[1].id").value(user2.getId()))
                .andExpect(jsonPath("$[1].name").value(user2.getName()))
                .andExpect(jsonPath("$[1].email").value(user2.getEmail()))
                .andExpect(jsonPath("$[1].password").value(user2.getPassword()));
    }

    @Test
    public void getUserById_found() throws Exception {
        User user = new User(1L, "John Doe", "john.doe@example.com", "p@ssW0rd!");
        when(userRepository.findById(user.getId())).thenReturn(Optional.of(user));
        mockMvc.perform(get("/api/users/{id}", user.getId()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(user.getId()))
                .andExpect(jsonPath("$.name").value(user.getName()))
                .andExpect(jsonPath("$.email").value(user.getEmail()))
                .andExpect(jsonPath("$.password").value(user.getPassword()));
    }

    @Test
    public void getUserById_notFound() throws Exception {
        Long userId = 1L;
        when(userRepository.findById(userId)).thenReturn(Optional.empty());
        mockMvc.perform(get("/api/users/{id}", userId))
                .andExpect(status().isNotFound());
    }
}

Este código es un conjunto de pruebas JUnit para la clase UserController. Las pruebas usan la clase MockMvc para simular solicitudes HTTP al endpoint /api/users y verificar que los métodos correspondientes de la clase UserController las gestionen correctamente. Las pruebas abarcan escenarios como crear un usuario con datos válidos e inválidos, obtener todos los usuarios y obtener un usuario por ID, tanto cuando existe como cuando no.

La anotación @SpringBootTest se usa para cargar el contexto de la aplicación y la anotación @MockBean se usa para simular la dependencia UserRepository de la clase UserController. En conjunto, estas pruebas garantizan que la clase UserController funcione correctamente y responda de forma adecuada a distintas solicitudes HTTP.

Para ejecutar las pruebas, abre una terminal o un shell, navega hasta el directorio raíz del proyecto y ejecuta el siguiente comando:

mvn test

Deberías ver que todas las pruebas se aprobaron:

…output omitted…
[INFO] Results:
[INFO] 
[INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] ------------------------------------------------------------------------
…output omitted…

Ejecutar y probar tus API

En IntelliJ, haz clic derecho en el archivo SimpleCrudBeanValidationApplication.java y selecciona Run 'SimpleCrudBeanValidationApplication.main()':

IntelliJ IDEA muestra una clase Java User con anotaciones de validación de beans y un menú contextual que destaca la opción para ejecutar la aplicación.

También puedes ejecutar la aplicación en la terminal o el shell con los siguientes comandos:

mvn clean install
java -jar target/simple-crud-bean-validation-0.0.1-SNAPSHOT.jar

Después de iniciar la aplicación Spring Boot, puedes probar las operaciones CRUD con comandos curl desde la terminal o el shell, de forma similar a como una aplicación frontend o Postman llamaría a estas API.

Para probar las operaciones CRUD, primero crea un usuario:

curl -X POST -H "Content-Type: application/json" -d '{"name":"John Doe","email":"john.doe@example.com","password":"p@ssW0rd!"}' http://localhost:8080/api/users

El resultado debería verse así:

{"id":1,"name":"John Doe","email":"john.doe@example.com","password":"p@ssW0rd!"} 

Luego, obtén todos tus usuarios:

curl -X GET http://localhost:8080/api/users

El resultado debería verse así:

{"id":1,"name":"John Doe","email":"john.doe@example.com","password":"p@ssW0rd!"}

A continuación, obtén el usuario por ID. Reemplaza <id> por el ID del usuario que quieres obtener:

curl -X GET http://localhost:8080/api/users/<id>

El resultado es el siguiente:

{"id":1,"name":"John Doe","email":"john.doe@example.com","password":"p@ssW0rd!"}

Actualiza el usuario reemplazando <id> por el ID del usuario que quieres actualizar:

curl -X PUT -H "Content-Type: application/json" -d '{"name":"John Doe Updated","email":"john.doe.updated@example.com","password":"newP@ssW0rd!"}' http://localhost:8080/api/users/<id>

El resultado debería verse así:

{"id":1,"name":"John Doe Updated","email":"john.doe.updated@example.com","password":"newP@ssW0rd!"}

Luego, elimina un usuario reemplazando <id> por el ID del usuario que quieres eliminar:

curl -X DELETE http://localhost:8080/api/users/<id>

El resultado de este comando está vacío.

Con estos comandos curl, puedes probar las operaciones CRUD de tu API RESTful de usuarios. Recuerda reemplazar <id> por el ID de usuario correspondiente en cada comando. Si sigues estos pasos, podrás interactuar con tu aplicación Spring Boot y verificar que las restricciones de Bean Validation funcionen como se espera durante la ejecución.

Si vuelves a ejecutar estas solicitudes curl con datos incorrectos, como un correo electrónico no válido, una contraseña no válida o valores vacíos, verás que la API devuelve un error de solicitud incorrecta. Este mensaje indica que la validación realizada en el nivel del controlador, también conocida como Bean Validation, no se superó.

Veamos algunos ejemplos de solicitudes incorrectas. Aquí, creas un usuario con una dirección de correo electrónico no válida:

curl -X POST -H "Content-Type: application/json" -d '{"name":"John Doe","email":"johexample","password":"p@ssW0rd!"}' http://localhost:8080/api/users

El resultado se ve así:

{"timestamp":"2023-03-28T10:27:21.884+00:00","status":400,"error":"Bad Request","path":"/api/users"}

Y aquí, creas un usuario con una contraseña no válida:

curl -X POST -H "Content-Type: application/json" -d '{"name":"John Doe","email":"joh@example.com","password":"test"}' http://localhost:8080/api/users

El resultado es el siguiente:

{"timestamp":"2023-03-28T10:29:30.793+00:00","status":400,"error":"Bad Request","path":"/api/users"}

Conclusión sobre la validación

En este artículo, aprendiste a implementar Bean Validation de Spring Boot en una aplicación CRUD sencilla. Creaste un proyecto con Spring Initializr, agregaste las dependencias necesarias e implementaste un modelo de usuario con anotaciones de Bean Validation. También implementaste un UserRepository y un UserController con endpoints de API RESTful que usan Bean Validation, y escribiste pruebas para verificar que las restricciones de Bean Validation funcionen como se espera.

Para mejorar aún más la seguridad de tu aplicación, usa Snyk Code, una herramienta SAST que detecta patrones de código inseguros que pueden provocar ataques de inyección. Puedes usar la extensión del plugin de Snyk Security para IntelliJ IDE para integrar este análisis en tu flujo de trabajo de desarrollo.

Ahora que entiendes cómo funciona Bean Validation de Spring Boot, puedes implementarlo en tus propios proyectos para garantizar la integridad de los datos y mejorar la experiencia del usuario. Para explorar el código completo, visita el siguiente repositorio de GitHub.

Comienza con Capture the Flag

Aprende a resolver desafíos de Capture the Flag con nuestro taller virtual 101 a pedido.

Leer más

blog feature snyk container custom base recommendations
Blog

Presentamos Evo Agentic AppSec: seguridad que sigue el ritmo del desarrollo con IA

El desarrollo impulsado por IA avanza más rápido que los flujos de trabajo de seguridad tradicionales. Agentic AppSec de Snyk usa agentes de seguridad para encontrar, corregir y verificar vulnerabilidades de forma continua, mientras las personas establecen el nivel.

feature insights context
Blog

Los ataques autónomos ya están aquí. La defensa debe estar a su altura.

Los atacantes autónomos están reduciendo el tiempo disponible para defenderse. Descubre cómo el descubrimiento, la corrección, la validación y la prevención continuos pueden ayudar a los equipos de seguridad a seguirles el ritmo.

illustration hero ai
Blog

El huracán de la IA ha llegado

La IA está acelerando por igual la creación de software y los ciberataques. Los líderes deben proteger los agentes y el código desde el inicio, aplicar controles en tiempo de ejecución y validar las defensas de forma independiente.