Se você esta iniciando sua carreira como Desenvolvedor(a) Java e utiliza Spring MVC para construir suas APIs REST já deve ter se perguntado o por quê não é utilizado a própria entidade para receber dados no Controller?
Para responder essa pergunta preciso te contar uma história bizarra que aconteceu com o GitHub. Em 2012 um hacker conseguiu explorar uma falha em um formulário de entrada, que permitiu ele se associar a organização do Ruby on Rails através de uma chave ssh publica. Após se associar este usuário possuía permissão necessária para fazer alterações nos repositórios da organização.
Isto aconteceu porquê provavelmente ele conseguiu persistir a sua chave de acesso na coleção de chaves permitidas da organização. Quando um usuário consegue alterar valores de objetos de maneira indesejada através de parâmetros de uma requisição HTTP, chamamos de Vulnerabilidade de Atribuição de Massa.
Para entender melhor imagine que um usuário mal-intencionado descobre que é possível cadastrar uma conta bancaria com um valor de saldo aprovado para uso através da requisição de criação de conta. Este usuário poderia simplesmente transferir este valor antes que a falha fosse identificada e causar sérios prejuízos ao banco.
Um exemplo de API que permita esta operação é:
@Entity
public class Conta{
@Id
@GenratedValue
private Long id;
private String titular;
private BigDecimal saldo;
}
@RestController
public class CadastrarConta{
@AutoWired
private ContaRepository repository;
@PostMapping("/contas")
public ResponseEntity<?> cadastar(@RequestBody Conta conta, UriComponentsBuilder uriBuilder){
repository.save(conta);
URI location = uriBuilder.path("conta/credito/{id}")
.buildAndExpand(conta.getId())
.toUri();
return ResponseEntity.created(location).build();
}
}
Por mais que o desenvolvedor especifique que para criar uma conta é necessário enviar apenas o nome do titular no corpo da requisição. Como no exemplo abaixo:
POST /contas
...
body: {
"titular": "Jordi Henrique Marques da Silva"
}
O usuário mal-intencionado poderia simplesmente informar o saldo na sua requisição e iniciar sua conta com valor disponível em operação. Por exemplo:
POST /contas
...
body: {
"titular": "Jordi Henrique Marques da Silva",
"saldo":1000000.00
}
E este é o motivo do porquê não recebemos a entidade como porta de entrada de dados no Controller, pois ficamos vulneráveis a esta falha de segurança.
Uma solução simples para este problema é inserir um objeto para representar as informações de cadastro. Este objeto é uma implementação do padrão de projeto Data Transfer Object (DTO). Neste objeto inserimos apenas os atributos referente ao cadastro da Conta, não permitindo que o usuário acesse os demais campos da entidade. Abaixo o exemplo para cadastro de conta:
public class CadastroContaDTO{
private String titular;
public Conta toModel(){
return new Conta(titular);
}
}
Para deixar o nosso Controller seguro agora basta substituir o tipo do objeto conta de Conta para CadastroContaDTO no método cadastrar e adaptar a lógica de cadastro.
@RestController
public class CadastrarConta{
@AutoWired
private ContaRepository repository;
@PostMapping("/contas")
public ResponseEntity<?> cadastar(@RequestBody CadastroContaDTO request, UriComponentsBuilder uriBuilder){
Conta conta = request.toModel();
repository.save(conta);
URI location = uriBuilder.path("conta/credito/{id}")
.buildAndExpand(conta.getId())
.toUri();
return ResponseEntity.created(location).build();
}
}
O Controller adaptado fica igual ao código apresentado acima, e além de não ficar vulnerável ao ataque de atribuição de massa, ter um DTO de entrada permite que os modelos evoluam de maneira independente, ou seja, minha entidade pode ganhar novos campos, sem que meu DTO seja alterado e vice-versa.
Ter um DTO de entrada ou saída pode trazer consigo também uma otimização de dados trafegados na rede, dado que o DTO contém apenas as informações necessárias para satisfazer uma funcionalidade, campos que antes eram serializados, trafegados e não utilizados, agora não fazem mais parte da mensagem.
Conclusão
Receber uma entidade no Controller pode trazer grandes danos a segurança da sua aplicação e seu negócio, porém, ao inserir um DTO para receber estas informações, proporciona diversas vantagens como maior segurança contra vulnerabilidades, desacoplamento entre os modelos da API e negócio, e ganhos em desempenho em relação aos dados trafegados na rede.
Top comments (1)
Artigo massa 👏👏👏!!! Gosto da utilização de DTOs, eles ajudam e muito na evolução do contrato da API, desacoplando o modelo do contrato da API, fornecendo a oportunidade de criar novas versões do contrato e tal. O legal é que com a versão LTS atual do Java, o Java 17 ☕, podemos utilizar Records, o que facilita bastante a criação de DTOs 😎 .Muito bom!!! Abraços 👍!!!