From 07cc96c6e8df42168699c210cf64fc720399e556 Mon Sep 17 00:00:00 2001 From: vimal-tech-dev Date: Wed, 4 Mar 2026 00:00:43 +0530 Subject: [PATCH] Add Redis distributed rate limiting (prod only). Signed-off-by: vimal-tech-dev --- pom.xml | 16 ++++ .../security/RedisRateLimitFilter.java | 85 +++++++++++++++++++ src/main/resources/ApiResponse.java | 10 --- src/main/resources/application-prod.yml | 6 ++ 4 files changed, 107 insertions(+), 10 deletions(-) create mode 100644 src/main/java/com/vimaltech/contactapi/security/RedisRateLimitFilter.java delete mode 100644 src/main/resources/ApiResponse.java diff --git a/pom.xml b/pom.xml index 8b1feca..368e4be 100644 --- a/pom.xml +++ b/pom.xml @@ -91,6 +91,22 @@ org.flywaydb flyway-database-postgresql + + com.bucket4j + bucket4j-core + 8.6.0 + + + + com.bucket4j + bucket4j-redis + 8.6.0 + + + + org.springframework.boot + spring-boot-starter-data-redis + diff --git a/src/main/java/com/vimaltech/contactapi/security/RedisRateLimitFilter.java b/src/main/java/com/vimaltech/contactapi/security/RedisRateLimitFilter.java new file mode 100644 index 0000000..3d15d9e --- /dev/null +++ b/src/main/java/com/vimaltech/contactapi/security/RedisRateLimitFilter.java @@ -0,0 +1,85 @@ +package com.vimaltech.contactapi.security; + +import io.github.bucket4j.Bandwidth; +import io.github.bucket4j.Bucket; +import io.github.bucket4j.BucketConfiguration; +import io.github.bucket4j.distributed.proxy.ProxyManager; +import io.github.bucket4j.redis.lettuce.cas.LettuceBasedProxyManager; + +import io.lettuce.core.RedisClient; +import io.lettuce.core.RedisURI; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.time.Duration; +import java.util.Optional; + +@Component +@Profile("prod") +public class RedisRateLimitFilter extends OncePerRequestFilter { + + private final ProxyManager proxyManager; + + public RedisRateLimitFilter( + @Value("${spring.data.redis.host}") String host, + @Value("${spring.data.redis.port}") int port + ) { + + RedisURI redisURI = RedisURI.Builder.redis(host) + .withPort(port) + .build(); + + RedisClient redisClient = RedisClient.create(redisURI); + + this.proxyManager = LettuceBasedProxyManager + .builderFor(redisClient) + .build(); + } + + private BucketConfiguration configuration() { + return BucketConfiguration.builder() + .addLimit(Bandwidth.simple(10, Duration.ofMinutes(1))) + .build(); + } + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + + if (!request.getRequestURI().equals("/api/v1/contact")) { + filterChain.doFilter(request, response); + return; + } + + String ip = Optional.ofNullable(request.getHeader("X-Forwarded-For")) + .map(s -> s.split(",")[0].trim()) + .orElse(request.getRemoteAddr()); + + Bucket bucket = proxyManager.builder() + .build(("ip:" + ip).getBytes(), this::configuration); + + if (bucket.tryConsume(1)) { + filterChain.doFilter(request, response); + } else { + response.setStatus(429); + response.setContentType("application/json"); + response.getWriter().write(""" + { + "success": false, + "message": "Too many requests. Please try again later." + } + """); + } + } +} \ No newline at end of file diff --git a/src/main/resources/ApiResponse.java b/src/main/resources/ApiResponse.java deleted file mode 100644 index 1ae0b7b..0000000 --- a/src/main/resources/ApiResponse.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.vimaltech.contactapi.dto; - -import java.time.LocalDateTime; - -public record ApiResponse( - boolean success, - String message, - LocalDateTime timestamp -) { -} \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 40cc8b3..a6335c0 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -5,6 +5,12 @@ spring: password: ${DB_PASSWORD:?DB_PASSWORD is required} driver-class-name: org.postgresql.Driver + data: + redis: + host: redis + port: 6379 + timeout: 2000 + jpa: hibernate: ddl-auto: validate