add rate limiting

This commit is contained in:
2026-04-14 15:23:26 -06:00
parent 0411e4be06
commit d43942fb76
3 changed files with 111 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
package com.petshop.backend.security;
import com.petshop.backend.exception.ApiErrorResponder;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.time.Duration;
import java.util.Map;
@Component
public class RateLimitFilter extends OncePerRequestFilter {
private static final Map<String, int[]> RULES = Map.of(
"/api/v1/auth/login", new int[]{10, 15},
"/api/v1/auth/register", new int[]{5, 60},
"/api/v1/auth/forgot-password", new int[]{3, 10},
"/api/v1/auth/reset-password", new int[]{10, 15}
);
private final RateLimiterService rateLimiterService;
private final ApiErrorResponder apiErrorResponder;
public RateLimitFilter(RateLimiterService rateLimiterService, ApiErrorResponder apiErrorResponder) {
this.rateLimiterService = rateLimiterService;
this.apiErrorResponder = apiErrorResponder;
}
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
String path = request.getRequestURI();
int[] rule = RULES.get(path);
if (rule != null) {
String ip = extractIp(request);
String key = path + ":" + ip;
if (!rateLimiterService.isAllowed(key, rule[0], Duration.ofMinutes(rule[1]))) {
apiErrorResponder.write(response, HttpStatus.TOO_MANY_REQUESTS,
"Too many requests. Please try again later.", null, path);
return;
}
}
filterChain.doFilter(request, response);
}
private String extractIp(HttpServletRequest request) {
String forwarded = request.getHeader("X-Forwarded-For");
if (forwarded != null && !forwarded.isBlank()) {
return forwarded.split(",")[0].trim();
}
return request.getRemoteAddr();
}
}

View File

@@ -0,0 +1,45 @@
package com.petshop.backend.security;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class RateLimiterService {
private final Map<String, Deque<Instant>> buckets = new ConcurrentHashMap<>();
public boolean isAllowed(String key, int maxRequests, Duration window) {
Instant now = Instant.now();
Instant windowStart = now.minus(window);
Deque<Instant> timestamps = buckets.computeIfAbsent(key, k -> new ArrayDeque<>());
synchronized (timestamps) {
while (!timestamps.isEmpty() && timestamps.peekFirst().isBefore(windowStart)) {
timestamps.pollFirst();
}
if (timestamps.size() >= maxRequests) {
return false;
}
timestamps.addLast(now);
return true;
}
}
@Scheduled(fixedDelay = 300_000)
public void evictStale() {
Instant cutoff = Instant.now().minus(Duration.ofHours(2));
buckets.entrySet().removeIf(entry -> {
Deque<Instant> timestamps = entry.getValue();
synchronized (timestamps) {
return timestamps.isEmpty() || timestamps.peekLast().isBefore(cutoff);
}
});
}
}

View File

@@ -31,15 +31,18 @@ import java.util.List;
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
private final RateLimitFilter rateLimitFilter;
private final UserDetailsService userDetailsService;
private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
private final RestAccessDeniedHandler restAccessDeniedHandler;
public SecurityConfig(JwtAuthenticationFilter jwtAuthFilter,
RateLimitFilter rateLimitFilter,
UserDetailsService userDetailsService,
RestAuthenticationEntryPoint restAuthenticationEntryPoint,
RestAccessDeniedHandler restAccessDeniedHandler) {
this.jwtAuthFilter = jwtAuthFilter;
this.rateLimitFilter = rateLimitFilter;
this.userDetailsService = userDetailsService;
this.restAuthenticationEntryPoint = restAuthenticationEntryPoint;
this.restAccessDeniedHandler = restAccessDeniedHandler;
@@ -75,6 +78,7 @@ public class SecurityConfig {
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authenticationProvider(daoAuthenticationProvider())
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(rateLimitFilter, JwtAuthenticationFilter.class);
http.addFilterAfter(activityLoggingFilter, JwtAuthenticationFilter.class);
return http.build();