Skip to main content

Como usar DTOs em Java para manter a segurança

Escrito por
feature java dto

11 de outubro de 2022

0 minutos de leitura

Os objetos de transferência de dados (DTOs, na sigla em inglês) em Java transportam dados entre subsistemas. Esse é um padrão de projeto corporativo para agregar dados. Seu principal objetivo é reduzir o número de chamadas ao sistema necessárias entre os subsistemas, diminuindo a sobrecarga gerada.

Neste artigo, explico como os DTOs são usados em aplicações Java modernas, quais benefícios podem trazer para sua aplicação e como ajudam a aumentar a segurança ao evitar vazamentos acidentais de dados.

O que são POJO, Java Bean e Value Object

Como o nome sugere, um Plain Old Java Object (POJO) é um objeto Java comum. Pode ser qualquer classe e não está sujeito a restrições específicas além das definidas pela linguagem Java. POJOs são criados para promover a reutilização e aumentar a legibilidade.

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);
   }
}

Um Java Bean é um POJO que segue o padrão JavaBean. De acordo com esse padrão, todas as propriedades são privadas e acessadas por métodos getter e setter. Além disso, deve haver um construtor sem argumentos, entre outros requisitos.

Isso significa que todos os Java Beans são POJOs, mas nem todos os POJOs são 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;
   }
}

Um Value Object é um objeto pequeno que representa uma entidade de dados simples. No entanto, normalmente ele não tem uma identidade. Os Value Objects ainda não estão disponíveis em Java, mas os mantenedores do JDK estão trabalhando para adicioná-los como parte do JEP 401. Por enquanto, precisamos criar um POJO para fazer esse trabalho.

Como implementar um objeto de transferência de dados

Um DTO pode ser implementado como um POJO — ou, nesse caso, como um Java Bean. O mais importante é que ele separe as responsabilidades entre entidades, como a camada de apresentação e o modelo de domínio, por exemplo.

Vamos usar um pequeno serviço REST para explicar. Imagine uma cafeteria com cafés e clientes. No sistema, cada um deles é uma entidade de domínio separada. Para descobrir qual é o café favorito de um cliente, vou criar uma API que fornece os dados agregados representados em FavoriteCoffeDTO.

Diagrama que mostra JSON, API, FavoriteCoffeeDTO e Controller na camada de apresentação conectados a Customer e Coffee na camada de domínio.

O código fica mais ou menos assim:

​​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;

}

Na implementação acima, separei a camada de domínio da camada de apresentação. Assim, meu controller pode mapear as duas entidades de domínio para o DTO.

Neste exemplo, defini os campos como privados, então preciso criar métodos getter e setter para acessá-los. Muitas pessoas optam por seguir o padrão JavaBean totalmente ou em parte ao criar DTOs. Isso não é obrigatório, é claro: você pode escolher o método que melhor atende às suas necessidades. Outras opções incluem tornar todos os campos públicos e acessá-los diretamente, como no exemplo abaixo, ou tornar o objeto imutável com um construtor que receba todos os argumentos e alguns métodos getter.

public class FavoriteCoffeeDTO {

   public String name;
   public List<String> coffees;

}

String name = favCoffeeDTO.name;

Por fim, se você atualizou para uma versão mais recente do Java, pode usar Java Records nos seus DTOs. Java Records são classes imutáveis e simples que fornecem automaticamente um construtor com todos os argumentos, métodos de acesso, toString() e hashCode(), sem que seja necessário defini-los. Isso deixa o código mais enxuto e legível. Vale notar que os records não seguem a especificação Java Bean, pois os métodos de acesso não têm o prefixo get nem set.

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

String name = favoriteCoffeeDTO.name();

O que torna um DTO bom?

O objetivo de um DTO é transportar dados entre processos. Por isso, um bom DTO contém apenas as informações necessárias para aquela parte específica do sistema. No exemplo da API, precisamos retornar somente o nome do cliente e o pedido do café favorito dele. Não é necessário incluir mais nada, como lógica de negócios. A recomendação geral é manter seus DTOs tão simples, pequenos e diretos quanto possível.

Além disso, depois de inicializado, o estado de um DTO não deve mudar nem evoluir. Por isso, uma estrutura de dados imutável é ideal para um DTO. Como ele transporta apenas dados que não devem ser alterados, um Java Record é uma ótima opção — especialmente porque bibliotecas de serialização JSON, como Jackson, dão suporte a Java Records.

Considerações de segurança para DTOs

Como vimos, esse padrão com DTOs desacopla o modelo de domínio da camada de apresentação. DTOs simples, que contêm apenas os dados necessários para determinado subsistema ou API e não incluem lógica de negócios, também podem aumentar a segurança.

Em muitas provas de conceito, vemos que os dados das entidades de domínio são expostos por completo. Isso pode disponibilizar dados desnecessários fora do sistema e causar possíveis vazamentos.

Imagine que nossa API tenha uma função para encontrar todos os clientes, mas não use um DTO:

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

Se o objeto de cliente for igual ao do exemplo anterior, já estaremos exibindo informações demais. Será que precisamos mesmo do id ou da lista de coffees favoritos? A situação piora se decidirmos adicionar mais informações ao cliente, como o endereço residencial.

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

}

Se não filtrarmos os dados no endpoint, podemos causar uma violação de dados, já que fornecer o nome completo e o endereço residencial é considerado uma violação de privacidade em muitos países. Desacoplar a API do modelo de dados com um DTO teria evitado isso, pois o controller responsável pelo mapeamento preencheria apenas os campos necessários no DTO. Mesmo que depois decidamos adicionar algo ao modelo de domínio, usar um DTO impede que informações de identificação pessoal sejam expostas.

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

Recomendações

Em geral, usar DTOs em Java para desacoplar subsistemas é uma boa ideia. Do ponto de vista da engenharia, isso reduz as idas e vindas entre as diferentes camadas. Do ponto de vista da segurança, ajuda a evitar vazamentos acidentais de dados. Recomendo criar DTOs específicos e limitar a reutilização. Se você reutilizar seus DTOs em diferentes funções, entenda claramente onde eles são usados antes de fazer alterações.

Em geral, mantenha seus DTOs concisos, sem lógica de negócios sempre que possível, e forneça apenas os dados necessários para funções específicas. Por fim, acredito que a imutabilidade combina naturalmente com DTOs. Por isso, Java Records — lançados oficialmente no Java 16 — são uma ótima forma de implementar DTOs em Java.

Confira os recursos a seguir para saber mais sobre segurança em Java:

Você também pode experimentar gratuitamente nossa ferramenta online de análise de código Java para ver como o Snyk Code analisa seu código em busca de problemas de segurança e qualidade.

Perguntas frequentes

O que é um DTO?

DTO é a sigla em inglês para Data Transfer Object (objeto de transferência de dados). É um objeto que transporta dados entre diferentes processos em uma aplicação. Um DTO foi projetado especificamente para reduzir o número de chamadas de método entre cliente e servidor, agregando dados. Ele também evita o acoplamento estreito entre entidades, como o modelo de domínio e a camada de apresentação de uma aplicação.

O que é um DTO em Java?

Em Java, um objeto de transferência de dados costuma ser um POJO (Plain Old Java Object, ou objeto Java simples), sem lógica de negócios. Geralmente, ele tem uma estrutura de dados plana, adequada especificamente à finalidade da transação entre os métodos em que é usado. Os DTOs em Java podem seguir a convenção JavaBean, com campos privados e métodos getter e setter, mas isso não é obrigatório.

DTO e POJO são a mesma coisa?

Resumindo, um DTO é um POJO, mas nem todo POJO é um DTO. Um objeto de transferência de dados é um POJO específico para transportar dados entre processos.

Usar DTOs é uma boa prática?

Sim. Quando bem implementados, os DTOs reduzem o acoplamento estreito e o número de chamadas de método, o que aumenta a segurança e reduz os vazamentos de dados.

Qual é a diferença entre DAO e DTO?

DAO é a sigla em inglês para Data Access Object (objeto de acesso a dados) e é usado em um padrão que desacopla a lógica de negócios da camada de persistência. DAOs costumam ser usados em operações CRUD, como atualizar, excluir e salvar. Um DTO é um objeto destinado exclusivamente a transportar dados entre subsistemas e não deve conter lógica de negócios.

Comece a jogar Capture the Flag

Aprenda a resolver desafios de Capture the Flag assistindo à gravação sob demanda do nosso workshop virtual introdutório.

Leia mais

blog feature ai lilac
Blog

Transforme seu backlog de vulnerabilidades em pull requests com remediação agentiva

O Evo Remediation Agent está em Open Preview e traz a resolução automatizada de dependências para o Evo Agentic AppSec. Agora, as equipes de AppSec podem executar campanhas contínuas de remediação

AI Background min crop
Blog

A IA está acionando todos os riscos que você aceitou: sete insights do painel de segurança de IA sem filtros

A IA está tornando os ataques mais baratos e rápidos, expondo riscos que as organizações já carregam. Quatro líderes de segurança explicam por que a defesa na velocidade das máquinas e a correção mais rápida são essenciais.

blog feature ai violet
Blog

VulnBump: agentes de IA conseguem corrigir dependências vulneráveis sem quebrar o build?

O VulnBump testa agentes de IA em 307 atualizações reais de dependências. Uma correção só é considerada válida se eliminar a vulnerabilidade e mantiver o build e os testes passando.