Marcio Cunha

Como proteger APIs contra força bruta usando Redis e Lua

Aprenda a implementar um limitador de taxa atômico e distribuído em suas APIs utilizando scripts Lua no Redis para barrar ataques de força bruta com alta eficiência.

Marcio Cunha•4 min
Também disponível em:EnglishEspañol
Resumo
  • Scripts Lua rodam de forma atômica no Redis, evitando condições de corrida em ambientes com grande concorrência.
  • O controle de requisições descentralizado protege serviços distribuídos sem sobrecarregar bancos de dados relacionais.
  • A contagem precisa por IP ou token impede tentativas automatizadas de adivinhação de senhas em endpoints críticos.
  • A separação clara entre a lógica de expiração e incremento garante baixo consumo de memória no cache.
  • Atratividades de performance tornam essa abordagem ideal para microsserviços sob tráfego intenso.

O desafio de controlar acessos em APIs sob ataque

Quando disponibilizamos uma API na internet, o maior pesadelo não é apenas o tráfego legítimo elevado, mas sim as tentativas maliciosas de adivinhação de senhas. Ataques de força bruta sobrecarregam servidores e consomem recursos preciosos tentando quebrar credenciais. Na prática, isso significa que precisamos de uma barreira na porta de entrada que consiga contar requisições por usuário ou IP em tempo real. Se deixarmos essa contagem para bancos de dados tradicionais, a lentidão gerada pelas consultas frequentes pode derrubar o próprio sistema que estamos tentando defender.

Por que soluções ingênuas falham em ambientes distribuídos

Muitos desenvolvedores tentam resolver o controle de acesso gravando o número de tentativas em variáveis na memória da própria aplicação ou em tabelas simples. O problema surge quando escalamos a API para múltiplos servidores rodando em paralelo atrás de um balanceador de carga. Cada máquina passa a enxergar apenas uma fração dos acessos, permitindo que um invasor multiplique as tentativas reais sem estourar o limite individual. Para resolver isso, precisamos de um repositório centralizado de alta velocidade, e é exatamente aqui que entra a arquitetura de como implementar CQRS assíncrono em APIs de alto tráfego para lidar com fluxo intenso de dados.

O papel do Redis e o perigo das condições de corrida

O Redis é um banco de dados em memória extremamente rápido, famoso por armazenar estruturas baseadas em chave e valor. Em cenários comuns, verificamos um contador com o comando GET, somamos um valor no código da aplicação e depois salvamos de volta com SET. Contudo, em ambientes altamente concorrentes, duas requisições paralelas podem ler o mesmo valor inicial ao mesmo tempo, resultando em uma contagem errada. Esse fenômeno é conhecido como condição de corrida. Na prática, o sistema deixa passar mais requisições do que deveria porque a leitura e a escrita não aconteceram em um único passo indissociável.

Garantindo atomicidade com scripts Lua no Redis

Para eliminar qualquer falha de sincronia, o Redis permite executar scripts escritos na linguagem Lua diretamente dentro do próprio servidor de banco de dados. Quando enviamos um script Lua para o Redis, ele executa todo o bloco de código de forma estritamente atômica. Nenhum outro comando consegue se intrometer ou alterar os dados enquanto o script estiver rodando. Isso significa que podemos verificar o limite atual, somar uma nova tentativa e definir o tempo de expiração em microssegundos, garantindo precisão absoluta mesmo quando centenas de usuários tentam logar simultaneamente.

Implementando o limitador de taxa na prática

Abaixo temos um exemplo clássico de script Lua que implementa um limitador de taxa por janela deslizante ou contagem com expiração. O código verifica se a chave existe, incrementa o valor e define um tempo de retenção caso seja a primeira tentativa daquele período. Para entender melhor como estruturar aplicações robustas que lidam com esse tipo de lógica e dependências externas, vale a pena conferir como aplicar Clean Architecture em APIs Go para manter o código limpo e testável.

local key = KEYS[1]
local limit = tonumber(ARGV[1])
local expire_time = tonumber(ARGV[2])

local current = redis.call('get', key)
if current and tonumber(current) >= limit then
    return 0
end

current = redis.call('incr', key)
if tonumber(current) == 1 then
    redis.call('expire', key, expire_time)
end

return current

Tratando falhas e mantendo a resiliência do sistema

Embora o Redis seja extremamente estável, redes podem falhar e servidores podem reiniciar inesperadamente. Se o cache cair, a sua API não deve parar de funcionar completamente apenas porque o limitador ficou indisponível. Na prática, aplicamos o conceito de falha aberta ou fechada dependendo da criticidade do endpoint de login. Em cenários de alta segurança, podemos optar por barrar temporariamente ou registrar um alerta crítico, similar aos princípios que utilizamos ao proteger microsserviços do efeito cascata com circuit breaker para evitar que um componente quebre toda a aplicação.

Considerações finais sobre segurança e performance

Proteger suas rotas de autenticação contra ataques automatizados é um pilar indispensável para manter a integridade de qualquer sistema moderno. O uso combinado do Redis com scripts Lua resolve o problema clássico da concorrência distribuída sem sacrificar a velocidade de resposta que os usuários esperam. Ao centralizar essa lógica no cache, você economiza recursos vitais do banco de dados principal e garante uma defesa sólida contra invasores persistentes.