Skip to main content

Cómo usar DTO de Java para mantener la seguridad

Escrito por
feature java dto

11 de octubre de 2022

0 minutos de lectura

Los objetos de transferencia de datos (DTO) en Java son objetos que transportan datos entre subsistemas. Son un patrón de diseño empresarial para agregar datos. Su propósito principal es reducir la cantidad de llamadas al sistema necesarias entre los subsistemas y, así, disminuir la sobrecarga generada.

En este artículo, explicaré cómo se usan los DTO en las aplicaciones Java modernas, qué beneficios pueden aportar a tu aplicación y cómo pueden ayudarte a mejorar la seguridad al prevenir filtraciones accidentales de datos.

Qué son un POJO, un Java Bean y un objeto de valor

Como su nombre lo indica, un objeto Java antiguo y simple (POJO, por sus siglas en inglés) es un objeto Java común. Puede ser cualquier clase y no está sujeto a restricciones específicas, más allá de las establecidas por el lenguaje Java. Se crean para facilitar la reutilización y mejorar la legibilidad.

public class CoffeePOJO {

   public String name;
   private List<String> ingredients;

   public CoffeePOJO(String name, List<String> ingredients) {
       this.name = name;
       this.ingredients = ingredients;
   }

   void addIngredient(String ingredient) {
       ingredients.add(ingredient);
   }
}

Un Java Bean es un POJO que cumple con el estándar JavaBean. Según este estándar, todas las propiedades son privadas y se accede a ellas mediante métodos getter y setter. Además, debe tener un constructor sin argumentos, entre otros requisitos.

Esto significa que todos los Java Beans son POJO, pero no todos los POJO son Java Beans.

public class CoffeeBEAN implements Serializable {

   private String name;
   private List<String> ingredients;

   public CoffeeBEAN() {
   }

   public String getName() {
       return name;
   }

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

   public List<String> getIngredients() {
       return ingredients;
   }

   public void setIngredients(List<String> ingredients) {
       this.ingredients = ingredients;
   }
}

Un objeto de valor es un objeto pequeño que representa una entidad de datos simple. Sin embargo, por lo general, no tiene identidad. Los objetos de valor aún no están disponibles en Java, pero quienes mantienen el JDK trabajan para agregarlos como parte de JEP 401. Por ahora, tenemos que crear un POJO que se encargue de esa tarea.

Implementar un objeto de transferencia de datos

Un DTO se puede implementar como un POJO o, también, como un Java Bean. Lo más importante es que un DTO separe las responsabilidades entre entidades, como la capa de presentación y el modelo de dominio, por ejemplo.

Veamos un pequeño servicio REST para explicarlo. Supongamos que tenemos una cafetería con cafés y clientes. Ambos son entidades de dominio independientes en el sistema. Si quiero saber cuál es el café favorito de un cliente, crearé una API que proporcione los datos agregados representados en FavoriteCoffeDTO.

Diagrama que muestra JSON, API, FavoriteCoffeeDTO y Controller en la capa de presentación, conectados con Customer y Coffee en la capa de dominio.

El código se vería más o menos así:

​​public class Coffee {

   private Long id;
   private String name;
   private List<String> ingredients;
   private String preparation;

}

public class Customer {

   private Long id;
   private String firstName;
   private String lastName;
   private List<Coffee> coffees;

}

public class FavoriteCoffeeDTO {

   private String name;
   private List<String> coffees;

}

En la implementación anterior, separé la capa de dominio de la capa de presentación. Así, mi controlador puede encargarse de asignar las dos entidades de dominio al DTO.

En este ejemplo, hice que los campos fueran privados, por lo que tengo que crear métodos getter y setter para acceder a ellos. Para los DTO, muchas personas optan por seguir el estándar JavaBean por completo o en parte. Claro que no es obligatorio: puedes elegir el método que mejor se adapte a tus necesidades. Otras opciones son hacer públicos todos los campos y acceder a ellos directamente (como en el ejemplo de abajo), o hacer que el objeto sea inmutable con un constructor que acepte todos los argumentos y algunos métodos getter.

public class FavoriteCoffeeDTO {

   public String name;
   public List<String> coffees;

}

String name = favCoffeeDTO.name;

Por último, si actualizaste a una versión más reciente de Java, quizá quieras usar records de Java para tus DTO. Los records de Java son clases inmutables y sencillas que proporcionan automáticamente un constructor que acepta todos los argumentos, métodos de acceso, toString() y hashCode(), sin que tengas que definirlos. Esto hace que tu código sea más conciso y legible. Ten en cuenta que los records no siguen la especificación Java Bean, ya que sus métodos de acceso no llevan el prefijo get ni set.

public record FavoriteCoffeeDTO(String name, List<String> coffees) {}

String name = favoriteCoffeeDTO.name();

¿Qué características tiene un buen DTO?

El propósito de un DTO es transportar datos entre procesos. Por lo tanto, un buen DTO solo contiene la información necesaria para esa parte específica del sistema. En el ejemplo de nuestra API, solo necesitamos devolver el nombre del cliente y su pedido de café favorito. No hace falta agregar nada más, como lógica de negocio. En general, se recomienda que los DTO sean lo más simples, pequeños y directos posible.

Además, una vez inicializado un DTO, su estado no debería cambiar ni evolucionar. Esto significa que una estructura de datos inmutable sería ideal para un DTO. Como un DTO solo transporta datos que no deberían modificarse, un record de Java sería una excelente opción, sobre todo porque las bibliotecas de serialización JSON, como Jackson, son compatibles con ellos.

Consideraciones de seguridad de los DTO

Ya vimos que este patrón de DTO desacopla el modelo de dominio de la capa de presentación. Los DTO simples, que solo contienen los datos necesarios para el subsistema o la API y no incluyen lógica de negocio, también pueden mejorar tu seguridad.

En muchas pruebas de concepto, vemos que se muestran por completo los registros de dominio. Esto puede hacer que haya datos innecesarios disponibles fuera del sistema y provocar posibles filtraciones.

Supongamos que nuestra API tiene una función para buscar a todos los clientes, pero no usa un DTO:

@GetMapping("/customers")
public List<Customer> getAllCustomers(){
   return repository.findAll();       
}

Si nuestro objeto de cliente es igual al del ejemplo anterior, ya estamos mostrando demasiada información. ¿Realmente necesitamos el id o la lista de coffees favoritos? La situación empeora si decidimos agregar más información del cliente, como su domicilio.

public class Customer {
   private Long id;
   private String firstName;
   private String lastName;
   private List<Coffee> coffees;
   Private String homeAddress;

}

Si no filtramos los datos en nuestro endpoint, podemos provocar una filtración de datos, ya que en muchos países mostrar el nombre completo y el domicilio se considera una violación de la privacidad. Desacoplar la API del modelo de datos mediante un DTO habría evitado este problema, porque el controlador de mapeo solo habría completado los campos necesarios del DTO. Incluso si después decidimos agregar algo a nuestro modelo de dominio, usar un DTO evita que filtremos, en la práctica, información de identificación personal.

public class CustomerDTO {
   private String firstName;
   private String lastName;
}

Recomendaciones

En general, usar DTO en Java para desacoplar subsistemas es una buena idea. Desde el punto de vista de ingeniería, reducirá los viajes de ida y vuelta entre las distintas capas y, desde el punto de vista de la seguridad, ayudará a prevenir filtraciones accidentales de datos. En cuanto a la seguridad, recomiendo que tus DTO sean específicos y que limites su reutilización. Si reutilizas tus DTO para distintas funciones, asegúrate de entender claramente dónde se usan antes de modificarlos.

En general, procura que tus DTO sean concisos, no incluyan lógica de negocio si es posible y proporcionen únicamente los datos necesarios para funciones específicas. Por último, considero que la inmutabilidad es una característica natural de los DTO. Por eso, los records de Java, disponibles desde Java 16, son una excelente forma de implementar DTO en Java.

Consulta estos recursos para obtener más información sobre la seguridad en Java:

También puedes probar gratis nuestra herramienta en línea para analizar código Java y ver cómo Snyk Code analiza tu código en busca de problemas de seguridad y calidad.

Preguntas frecuentes

¿Qué es un DTO?

DTO significa objeto de transferencia de datos. Es un objeto que transporta datos entre distintos procesos de una aplicación. Un DTO está diseñado específicamente para reducir la cantidad de llamadas a métodos entre el cliente y el servidor mediante la agregación de datos. También evita el acoplamiento estrecho entre entidades, como el modelo de dominio y la capa de presentación de una aplicación.

¿Qué es un DTO en Java?

Un objeto de transferencia de datos en Java suele ser un POJO (objeto Java antiguo y simple) sin lógica de negocio. Por lo general, tiene una estructura de datos plana, diseñada específicamente para la transacción entre los métodos donde se usa. Los DTO en Java pueden seguir la convención JavaBean, con campos privados y métodos getter y setter, pero no es obligatorio.

¿Un DTO es lo mismo que un POJO?

En resumen, un DTO es un POJO, pero no todos los POJO son DTO. Un objeto de transferencia de datos es un POJO específico para transportar datos entre procesos.

¿Usar DTO es una buena práctica?

Sí. Si se implementan bien, los DTO reducen el acoplamiento estrecho y la cantidad de llamadas a métodos, lo que mejora la seguridad y reduce las filtraciones de datos.

¿Cuál es la diferencia entre un DAO y un DTO?

DAO significa objeto de acceso a datos y se usa en un patrón para desacoplar la lógica de negocio de la capa de persistencia. Los DAO suelen usarse para operaciones CRUD, como actualizar, eliminar y guardar. Un DTO es un objeto destinado exclusivamente a transportar datos entre subsistemas y no debería contener lógica de negocio.

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 ai lilac
Blog

Convierte tu backlog de vulnerabilidades en pull requests con la remediación agéntica

Evo Remediation Agent ya está disponible en Open Preview e incorpora la resolución automatizada de dependencias a Evo Agentic AppSec. Ahora, los equipos de AppSec pueden ejecutar campañas de remediación continuas

AI Background min crop
Blog

La IA activa todos los riesgos que has aceptado: siete ideas del panel de seguridad de IA sin filtros

La IA hace que los ataques sean más baratos y rápidos, y deja al descubierto riesgos que las organizaciones ya enfrentan. Cuatro líderes de seguridad explican por qué la defensa a velocidad de máquina y la corrección más rápida son esenciales.

blog feature ai violet
Blog

VulnBump: ¿Pueden los agentes de IA corregir dependencias vulnerables sin romper la compilación?

VulnBump prueba agentes de IA con 307 actualizaciones reales de dependencias. Una corrección cuenta solo si elimina la vulnerabilidad y la compilación y las pruebas siguen pasando.