Blindando sua API com o padrão DTO
Falaaa galerinha blza? , pois bem, como estamos na era das API´s e MS, entendi que seria ideal falar um pouco sobre a manipulação de dados com o padrão DTO, até mesmo proteger sua API.
Quando se está criando uma API é de extrema importância não apenas pensar como um usuário regular irá interagir, mas também se defender contra usuários maliciosos para que eles não consigam causar nenhum prejuízo para a sua aplicação e para seus usuários.
Mãos a OBRA !
Temos uma API, escrita em Java, usando o framework Spring, que é usada pelos funcionários da nossa empresa para que eles possam “bater ponto”, ou seja, para informar o horário de entrada e saída dos mesmos.
Para que nossa aplicação funcione, é necessário que os funcionários façam um cadastro no sistema. Cada funcionário é representado pela seguinte classe:
Temos o nosso Controller:
Para acessar nosso banco de dados temos a classe UsuarioRepository:
@Repository: Faz o framework enxergar nossa classe e indicamos que se trata de um repositório, ou seja, uma classe que tem como única função acessar o banco de dados.
Vale uma explicação que, ao extender CrudRepository automaticamente herdamos vários métodos, que para nosso exemplo já serão suficiente.
Vamos olhar nossa classe de serviço e ver como ela realiza a tarefa de salvar um novo usuário:
@Service: Usamos esta anotação para que o framework enxergue nossa classe e indicamos que esta classe é um serviço.
Parece simples, recebemos um usuário como parâmetro e devolvemos o usuário criado. O método save() nos devolve o objeto salvo, logo salvamos e já retornamos o objeto.
Nada mais justo que testarmos nossa aplicação, certo?
Para isso vamos usar o Postman e enviar uma requisição para nosso método salvar:
Enviando esta requisição, temos a seguinte resposta:
Funcionou perfeitamente, o objeto foi salvo! Podemos perceber isso pelo atributo id, vejam que ele foi gerado automaticamente.
Vamos fazer um outro teste. E se eu mandasse esta requisição:
Nesta nova requisição estou enviando um parâmetro extra, estou enviando o atributo admin passando o valor true.
Pensando em como o sistema deve funcionar ele não deveria permitir isso, certo? Fornecer permissões de administrador para um usuário deve ser uma função separada com sua própria lógica, portanto, nosso método que salva um usuário não deve permitir tal parâmetro. Vamos ver:
Vish… acabamos de criar um usuário com permissões de administrador. Isso não é bom.
Esse é um problema que algumas aplicações possuem por receber o objeto real diretamente como parâmetro do método que recebe a requisição, no nosso caso, o salvar() do nosso controller.
O Jackson, framework usado para serialização/desserialização, compara os atributos do JSON com os atributos da classe do objeto como parâmetro do método que está recebendo a requisição, no caso, o objeto usuario.
Com isso, basicamente, todos os atributos do JSON que tiverem um correspondente do mesmo nome na classe Usuario serão preenchidos.
Pensando numa aplicação real que possui este mesmo contexto, quão difícil seria para um usuário malicioso descobrir que tal classe possui um atributo booleano chamado admin?
Ao invés do Postman, em um formulário de cadastro, quão difícil seria adicionar um input com o tipo hidden passando admin igual a true?
Se “googlarmos” um pouco a respeito, vamos achar algumas notícias falando que tal empresa foi invadida usando exatamente esta técnica.
Como nos proteger disso?
Aplicando o padrão DTO
Podemos usar um conhecido padrão de projeto chamado DTO (Data Transfer Object), que basicamente é uma classe com atributos simples, que usamos para otimizar a comunicação entre o client e o servidor.
Nossa classe DTO pode receber atributos e, assim, podemos manipulá-los da forma que quisermos.
Para melhor compreensão vamos criar a classe UsuarioDTO e vamos recebê-la como parâmetro do método salvar() da classe UsuarioController:
Recomendados pelo LinkedIn
UsuarioController:
Fazendo isso recebemos erro de compilação pois o método salvar do nosso serviço precisa receber um objeto do tipo Usuario e não UsuarioDTO. Depois arrumamos isso.
Perceba que agora a desserialização do JSON ocorrerá com a classe UsuarioDTO, logo precisamos escolher quais atributos queremos receber. Basicamente queremos todos, menos o admin e o id, certo? Então vamos adicionar os atributos escolhidos:
Agora precisamos passar objeto do tipo Usuario para o método do nosso serviço e não um UsuarioDTO. Vamos ensinar nossa classe UsuarioDTO a criar um objeto do tipo Usuario. Primeiro vamos criar um construtor na classe Usuario:
Acabamos de sobrescrever o construtor padrão da classe, então, devemos adicionar um construtor vazio para que o Hibernate não reclame na hora que formos fazer operações com o banco de dados.
@NoArgsContructor: Adiciona um construtor vazio.
Agora vamos criar um método na classe UsuarioDTO que transforma o DTO em um objeto do tipo Usuario:
Simples não? Só nos resta invocar este método no nosso controller, para que, assim, passemos um objeto do tipo Usuario para o método do serviço:
Perceba que agora, mesmo que o usuário tente enviar um atributo admin não irá funcionar, pois nossa classe UsuarioDTO não possui este atributo. Vamos testar:
Novamente estamos enviando o atributo admin passando o valor true. Como resposta dessa requisição, temos:
Funcionou! Olha o atributo admin com valor false.
Tem outra coisa que está me incomodando, é estranho retornar a senha, não? Normalmente a senha é encriptada e salva no banco de dados, não deveríamos enviar este atributo que em teoria deve ser bem protegido.
Por outro lado, é imprescindível retornar as informações do usuário que acabamos de criar.
Usando o padrão DTO para customizar respostas HTTP
Podemos criar um DTO de resposta também. Criamos uma classe com os atributos que queremos enviar para o nosso client e transformamos o usuário criado nesse DTO de resposta.
No caso queremos todos os atributos menos a senha, certo? Então, primeiro colocaremos todos os atributos que queremos retornar nessa nova classe:
Agora criamos um método que recebe um objeto do tipo Usuario e retorna esta própria classe:
E finalmente, no método do nosso controller, só nos resta transformar nosso objeto usuario em um objeto do tipo UsuarioRespostaDTO e mudar o tipo englobado pelo ResponseEntity:
Testando nosso DTO de resposta, temos:
Pronto, agora a senha não está mais sendo enviada.
O padrão de projeto DTO é muito útil tanto para receber dados quanto para enviá-los, pois podemos manipular da forma que quisermos tais dados para facilitar a comunicação entre o servidor e o client.
Caso tenha ficado alguma dúvida ou tenha alguma sugestão, por favor comente aqui, tentarei responder o mais rápido possível.
Desenvolvedor Java Back-End na PrimeIT
2 aParabens pelo conteudo
DevOps | CI/CD | Docker | Linux | Kubernetes | Github Actions | Azure | ArgoCD
3 aConteúdo agregador, vlw Edilson Freitas