Backend Seguro — Rate Limit com Spring Boot em múltiplas instâncias

Christiano Guedes
CoreShield
Published in
7 min readJul 17, 2020
Imagem por: epictop10.com

Introdução

Hoje em dia é muito comum termos aplicações que funcionam online, em navegadores ou dispositivos móveis. Com isso é necessário proteger todos os serviços para que não ocorram vazamentos e acessos indevidos que poderiam prejudicar a imagem da solução ou até elevando consideravelmente o custo de cloud.

Através da técnica de Rating Limit é possível impor limites na quantidade de requisições por um intervalo de tempo para um cliente/ip. Essa camada de segurança protege as APIs contra:

  • DDoS: Ataque de negação de serviço que tem o objetivo de deixar seu sistema offline.
  • Brute Force: Ataque de força bruta muito utilizado para descobrir senhas de acessos.
  • Uso excessivo da aplicação: Normalmente feito por bots.

Em uma arquitetura de micro serviço feita em java/kotlin e com o framework Spring é possível de maneira simples, sem aumento significativo da perfomance, com pouco código adicionado e sem nenhuma modificação na regra de negócio, implementar de forma distribuída e em todas as instancias o Rate Limiting.

Conhecendo o Hazelcast: Dados distribuídos

O Hazelcast é uma ferramenta open-source focada em computação distribuída e é feita em Java. Isso mesmo, através de um pequeno jar na dependência do projeto temos uma maneira muito simples de distribuir o processamento de dados entre os nós.

Os dados ficam compartilhados em memória entre todas as instâncias da aplicação através de maps, por isso é uma ferramenta muito utilizada para cache compartilhado e com alta performance.

A ferramenta Hazelcast é disponibilizada em duas versões sendo uma community e outra enterprise, que possui alguns recursos mais elaborados. Ainda assim, no escopo desse artigo a versão adotada será a community, de fácil acesso a todos.

Por padrão e para facilitar a configuração o Hazelcast utiliza UDP para realizar discovery de máquinas na rede e assim adicionar ao cluster automaticamente por multicast, porém se preferir também é possível configurar a comunicação via TCP/IP.

Para o rate limiting implementado neste artigo o Hazelcast será utilizado no escopo de cache em memória para compartilhamento entre os nós do mapa das requisições feitas para cada cliente conforme a topologia abaixo.

Fonte Imagem: https://reflectoring.io/spring-boot-hazelcast/

Conhecendo o Bucket4J: Rate limiting

O Bucket4J é uma biblioteca feita em Java que implementa o rate limit através do token-bucket algorithm.

"Podemos considerar um Token Bucket como um balde furado em que a fila de pacotes pode ser considerada como o volume do balde. Independente de como cheguem os dados na fila, a saída é sempre uniforme em vazão e latência. Se a capacidade da fila for ultrapassada (volume de pacotes for maior que o volume do balde) então os pacotes serão descartados, o balde transborda os pacotes em excesso." Referencia: https://pt.wikipedia.org/wiki/Token_bucket

Fonte: https://golb.hplar.ch/2019/08/rate-limit-bucket4j.html

Também nesse artigo será utilizada a biblioteca bucket4j-spring-boot-starter para uma fácil integração do Bucket4j com o Spring Boot através da configuração de properties.

Primeiro passo: Adicionando as dependências no Spring Boot

Conforme dito anteriormente é necessário habilitar o cache do Spring e adicionar o Hazelcast e também o Bucket4J como dependências ao pom.xml do projeto, conforme exemplo do trecho abaixo.

pom.xml

Segundo passo: Configuração do cache

Para habilitar o cache no Spring Boot é necessário adicionar em uma classe de configuração a anotação @EnableCaching.

Para o Hazelcast, deve-se criar o mapa de cache que será compartilhado entre os nós, através da criação do arquivo hazelcast.xml no resources da aplicação.

O valor do XML deve seguir as informações abaixo:

Nesse exemplo criamos o cache via map chamado "rate-limit"

Para que o Spring reconheça o arquivo de configuração do Hazelcast é preciso adicionar no application.properties a propriedade:

spring.cache.hazelcast.config=classpath:hazelcast.xml

Já para o Bucket4J funcionar com as versões mais nova do Spring Boot tem que habilitar a sobrecarga de beans de um mesmo qualifier, através da propriedade abaixo:

spring.main.allow-bean-definition-overriding=true

Ficará assim:

A configuração do cache ficará assim no application.properties.

Terceiro passo: Regra padrão do limite de requisições na aplicação por IP

Para a configuração de uma regra default é preciso habilitar algumas propriedades no application.properties da aplicação, neste exemplo do artigo temos uma regra de 5 acessos por minuto para um mesmo IP e após excedidas as requisições ocorre um bloqueio de 1 minuto.

Observação: remover os comentários ao colocar no código para correto funcionamento, pois são apenas explicativos.

Dica adicional: Se a aplicação tem uma capacidade limitada de recursos é possível utilizar o rate limiting para bloquear o excedente para todas as requisições de origens distintas, para isso basta remover a propriedade bucket4j.filters[0].rate-limits[0].expression

Para testar a configuração é necessário ter pelo menos um Controller, segue abaixo o exemplo:

Teste do limite de requisições no 'cluster' da aplicação

Após o build realizado com sucesso e para simular um cluster da aplicação na própria máquina local será necessário iniciar várias instâncias da aplicação em portas distintas como no exemplo abaixo:

  • Instância 1:java -jar -Dserver.port=8080 rate-limit-java.jar
  • Instância 2:java -jar -Dserver.port=8081 rate-limit-java.jar
  • Instância 3:java -jar -Dserver.port=8082 rate-limit-java.jar
Três instâncias em portas distintas indicadas pela seta.

A cada nova instância que finaliza o carregamento é exibido nos logs das aplicações o novo nó no cluster do Hazelcast, conforme demostrado abaixo.

Nesse exemplo temos três instâncias no Hazelcast.

Para a o teste das requisições serão feitas 6 chamadas entre instâncias (portas) distintas para o endpoint "/" em um intervalo de menos de 1 minuto.

1. Requisição na instância de número 1 (porta 8080):

O Bucket4J por padrão retorna no header da resposta a quantidade de requisições restantes.

2. Requisição na instância de número 2 (porta 8081):

3. Requisição na instância de número 3 (porta 8082):

E assim segue até a sexta requisição quando ocorre o bloqueio.

Quarto passo: Regra de limite com validação usando Beans e regex por URL (opcional)

Além da restrição de acesso de todos os endpoints da aplicação por IP é possível limitar especificamente algumas URLs para um bloqueio mais restritivo. É muito útil para recursos mais sensíveis, por exemplo nos serviços vinculados à autenticação para evitar tentativa de força bruta, através da regex da propriedade do exemplo: bucket4j.filters[0].url=/auth/.*

Também é possível com o Bucket4J bloquear um usuário com muitos acessos mesmo tendo IPs distintos, através da utilização da execução de beans. Exemplo da propriedade:bucket4j.filters[0].rate-limits[0].expression

É importante observar que o Bucket4J tem uma configuração padrão que a primeira regra encontrada será a utilizada, portanto é recomendado colocar a regra global como sendo a de menor prioridade e para isso é preciso renomear as propriedades iniciadas porbucket4j.filters[0] para bucket4j.filters[1].

Para as duas regras restritivas o application.properties ficará conforme o trecho abaixo:

Observação: remover os comentários ao colocar no código para correto funcionamento, pois são apenas explicativos.

A simulação do teste é exatamente igual a etapa anterior, apenas trocando o endpoint final.

Exemplo de requisição:

curl -i -H "Content-Type: application/json" -X POST -d '{"oldPassword": "admin123", "newPassword": "s3cr3t"}' http://localhost:8080/auth/reset-password

Na quarta execução é bloqueado por 5 minutos conforme regra especificada no properties.

Exemplo de execução de bloqueio.

Quinto passo: Métricas de bloqueios (opcional)

Com o Bucket4j é possível obter métricas dos acessos negados pelo rate limiting através do Actuator que juntamente com o Prometheus pode criar um painel dashboard em um Grafana para monitorar e gerar alertas sobre tentativas de acessos indevidos ou possíveis tentativas de ataque ao sistema.

O foco deste artigo é apenas a liberação dos dados do Bucket4j com o Actuator, mas para entender como configurar o Prometheus com o Grafana no Spring Boot veja a recomendação de leitura aqui.

Para habilitar o Actuator deve adicionar a dependência no pom.xml conforme exemplo abaixo:

E também no application.properties deve adicionar a seguinte configuração:

Observação: remover os comentários ao colocar no código para correto funcionamento, pois são apenas explicativos.

Após a subida da aplicação e execução das requisições é possível obter as métricas dos bloqueios do Actuator conforme imagem abaixo:

Resultado: Métrica de bloqueio do Bucket4J

Código final

O código utilizado nesse artigo pode ser clonado em: https://github.com/christianoguedes/rate-limit-java

Conclusão

O Rate Limiting é uma técnica essencial para proteger qualquer sistema e o Bucket4J juntamente com o Hazelcast são uma alternativa simples para o Spring Boot sem ter que "poluir" o código, nem ter que alterar nenhuma regra de negócio e são também de fácil escalabilidade em instâncias distribuídas.

Também é possível ter um grande poder de personalização das regras de bloqueios com a execução dos beans, algo que normalmente não é possível em um rate limiting externo a aplicação.

É sempre importante levar em conta que a cyber segurança é composta por camadas e o rate limiting é apenas uma dessas etapas e técnicas. Portanto é recomendado que a aplicação tenha outras medidas como por exemplo bloqueio por captcha nas retentativas de autenticação e na recuperação de senha.

--

--