"Rate limiting é a primeira linha de defesa contra abuso de API. Redis torna isso rápido e escalável." — #30DiasJava Security Notes
🎯 Objetivo do Day 20
Para proteger a API contra abuso e garantir fair usage, o Day 20 do #30DiasJava implementou Rate Limiting usando Redis, limitando requisições por endereço IP e retornando headers informativos sobre limites e disponibilidade.
🛠️ O que foi implementado
✅ Redis-based Rate Limiting
- IP-based limiting: Limitação baseada em endereço IP do cliente
- Sliding window: Janela deslizante de 60 segundos
- Configurable limits: Limite configurável (padrão: 100 req/min)
- Redis storage: Armazenamento de contadores em Redis
✅ Request Filtering
- Spring Filter: Filtro
OncePerRequestFilterpara interceptar todas as requisições - Conditional activation: Ativo apenas quando Redis está disponível
- Graceful degradation: Sistema funciona normalmente se Redis não estiver disponível
✅ Rate Limit Headers
- X-RateLimit-Limit: Limite máximo de requisições por período
- X-RateLimit-Remaining: Requisições restantes no período atual
- HTTP 429: Status code "Too Many Requests" quando limite excedido
✅ IP Address Detection
- X-Forwarded-For: Suporte para proxies e load balancers
- X-Real-IP: Suporte para nginx reverse proxy
- Remote address fallback: Fallback para IP remoto direto
📊 Arquitetura
┌─────────────────────────────────────────────────────────┐
│ Client Request │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ RateLimitingFilter │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 1. Extract IP Address │ │
│ │ 2. Check Redis Counter │ │
│ │ 3. Increment Counter │ │
│ │ 4. Add Rate Limit Headers │ │
│ │ 5. Block if Limit Exceeded (HTTP 429) │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Redis │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Key: "rate_limit:192.168.1.1" │ │
│ │ Value: "45" (request count) │ │
│ │ TTL: 60 seconds │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Spring Security Filter Chain │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Continue to next filter / controller │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
💻 Implementação
Dependências (pom.xml)
<!-- Cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
RedisConfig
@Configuration
@ConditionalOnProperty(name = "spring.data.redis.host", matchIfMissing = false)
public class RedisConfig {
@Value("${spring.data.redis.host:#{null}}")
private String redisHost;
@Value("${spring.data.redis.port:6379}")
private int redisPort;
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
if (redisHost == null || redisHost.trim().isEmpty()) {
throw new IllegalStateException("Redis host não configurado");
}
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName(redisHost);
config.setPort(redisPort);
return new LettuceConnectionFactory(config);
}
@Bean
public RedisTemplate<String, String> redisTemplate(
RedisConnectionFactory connectionFactory) {
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
RateLimitingFilter
@Component
@ConditionalOnBean(RedisTemplate.class)
public class RateLimitingFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(
RateLimitingFilter.class
);
@Autowired(required = false)
private RedisTemplate<String, String> redisTemplate;
// Configuration
private static final int MAX_REQUESTS_PER_MINUTE = 100;
private static final String RATE_LIMIT_KEY_PREFIX = "rate_limit:";
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
// Skip rate limiting if Redis is not available
if (redisTemplate == null) {
filterChain.doFilter(request, response);
return;
}
String clientIp = getClientIpAddress(request);
String rateLimitKey = RATE_LIMIT_KEY_PREFIX + clientIp;
// Check current request count
ValueOperations<String, String> ops = redisTemplate.opsForValue();
String currentCount = ops.get(rateLimitKey);
int count = currentCount != null ? Integer.parseInt(currentCount) : 0;
if (count >= MAX_REQUESTS_PER_MINUTE) {
log.warn("Rate limit exceeded for IP: {}", clientIp);
response.setStatus(429); // Too Many Requests
response.setContentType("application/json");
response.getWriter().write(
"{\"error\":\"Rate limit exceeded\"," +
"\"message\":\"Too many requests. Please try again later.\"}"
);
return;
}
// Increment counter
if (count == 0) {
ops.set(rateLimitKey, "1", 60, TimeUnit.SECONDS);
} else {
ops.increment(rateLimitKey);
}
// Add rate limit headers
response.setHeader("X-RateLimit-Limit",
String.valueOf(MAX_REQUESTS_PER_MINUTE));
response.setHeader("X-RateLimit-Remaining",
String.valueOf(MAX_REQUESTS_PER_MINUTE - count - 1));
filterChain.doFilter(request, response);
}
private String getClientIpAddress(HttpServletRequest request) {
// Check X-Forwarded-For header (for proxies/load balancers)
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
// Check X-Real-IP header (for nginx reverse proxy)
String xRealIp = request.getHeader("X-Real-IP");
if (xRealIp != null && !xRealIp.isEmpty()) {
return xRealIp;
}
// Fallback to remote address
return request.getRemoteAddr();
}
}
SecurityConfig Integration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private RateLimitingFilter rateLimitingFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) {
http
.addFilterBefore(rateLimitingFilter,
UsernamePasswordAuthenticationFilter.class)
// ... outras configurações
;
return http.build();
}
}
Configuration (application.yml)
spring:
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
username: ${REDIS_USERNAME:}
password: ${REDIS_PASSWORD:}
📈 Fluxo de Uso
1. Request Normal (dentro do limite)
# Request 1-100 no mesmo minuto
curl http://localhost:8080/api/familias
# Response Headers:
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
# Request 50
curl http://localhost:8080/api/familias
# Response Headers:
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 50
2. Request Excedendo Limite
# Request 101 (limite excedido)
curl http://localhost:8080/api/familias
# Response:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
{
"error": "Rate limit exceeded",
"message": "Too many requests. Please try again later."
}
3. Redis Key Structure
# Redis keys
redis-cli
# Ver contador de um IP
GET rate_limit:192.168.1.1
# "45"
# Ver TTL restante
TTL rate_limit:192.168.1.1
# 35 (segundos restantes)
# Listar todos os rate limits ativos
KEYS rate_limit:*
# 1) "rate_limit:192.168.1.1"
# 2) "rate_limit:10.0.0.5"
# 3) "rate_limit:172.16.0.10"
📊 Métricas e Observabilidade
Logs
2025-11-20 10:15:23.456 WARN RateLimitingFilter : Rate limit exceeded for IP: 192.168.1.100
2025-11-20 10:15:24.123 INFO RateLimitingFilter : Rate limit check passed for IP: 10.0.0.5
Redis Monitoring
# Monitorar comandos Redis em tempo real
redis-cli MONITOR
# Output:
1658324523.456789 [0 127.0.0.1:54321] "GET" "rate_limit:192.168.1.1"
1658324523.457123 [0 127.0.0.1:54321] "INCR" "rate_limit:192.168.1.1"
🎓 Lições Aprendidas
✅ O que funcionou bem
- Redis é rápido: Operações de incremento e verificação são muito rápidas
- TTL automático: Redis expira keys automaticamente, não precisa limpeza manual
- Graceful degradation: Sistema funciona mesmo se Redis não estiver disponível
- IP detection: Suporte a proxies e load balancers via headers
⚠️ Desafios e soluções
- Distributed rate limiting: Com múltiplas instâncias, cada uma tem seu próprio contador. Solução: usar Redis compartilhado (já implementado).
- Rate limiting por usuário: Atualmente apenas por IP. Para rate limiting por usuário autenticado, usar
userIdem vez de IP. - Burst protection: Sliding window permite bursts. Para proteção contra bursts, considerar token bucket algorithm.
📚 Recursos
- Spring Data Redis: https://docs.spring.io/spring-data/redis/docs/current/reference/html/
- Redis Commands: https://redis.io/commands/
- HTTP 429 Status Code: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429
- Rate Limiting Strategies: https://konghq.com/blog/how-to-design-a-scalable-rate-limiting-algorithm
🔗 Links
Next episode → Day 21/30 — OpenAPI/Swagger Documentation