Background
The current RRateLimiter operates per userId. A bot that rotates user IDs can still flood the system — each new userId gets a fresh limiter. A second layer of per-IP rate limiting at the filter level catches this pattern before it even reaches the service.
Proposed Change
Add IpRateLimitFilter (ordered before CorrelationIdFilter) using Redisson RRateLimiter keyed on the client IP.
String ip = Optional.ofNullable(request.getHeader("X-Forwarded-For"))
.map(h -> h.split(",")[0].strip())
.orElse(request.getRemoteAddr());
RRateLimiter limiter = redissonClient.getRateLimiter("ip_rate:" + ip);
limiter.trySetRate(RateType.OVERALL, config.requestsPerMinute(), config.keyTtl());
if (!limiter.tryAcquire(1)) {
response.setStatus(429);
return;
}
Config
flashsale:
ip-rate-limit:
requests-per-minute: 60
key-ttl: PT2M```
## Security Note
Always prefer the first token from `X-Forwarded-For` (the original client) but be aware this header can be spoofed unless set by a trusted reverse proxy. Document this in the filter's Javadoc.
## Acceptance Criteria
- [ ] `IpRateLimitFilter` implemented and registered at `@Order(0)` (before correlation ID)
- [ ] Config added to `FlashSaleProperties` and `application.yml`
- [ ] Unit test verifies 429 is returned after IP limit is reached
- [ ] Separate counter metric `flashsale.ip.rate_limited_total` incremented
- [ ] Behaviour documented in README security section
Background
The current
RRateLimiteroperates peruserId. A bot that rotates user IDs can still flood the system — each new userId gets a fresh limiter. A second layer of per-IP rate limiting at the filter level catches this pattern before it even reaches the service.Proposed Change
Add
IpRateLimitFilter(ordered beforeCorrelationIdFilter) using RedissonRRateLimiterkeyed on the client IP.Config