add rate limiting
This commit is contained in:
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,15 +31,18 @@ import java.util.List;
|
|||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
private final JwtAuthenticationFilter jwtAuthFilter;
|
private final JwtAuthenticationFilter jwtAuthFilter;
|
||||||
|
private final RateLimitFilter rateLimitFilter;
|
||||||
private final UserDetailsService userDetailsService;
|
private final UserDetailsService userDetailsService;
|
||||||
private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
|
private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
|
||||||
private final RestAccessDeniedHandler restAccessDeniedHandler;
|
private final RestAccessDeniedHandler restAccessDeniedHandler;
|
||||||
|
|
||||||
public SecurityConfig(JwtAuthenticationFilter jwtAuthFilter,
|
public SecurityConfig(JwtAuthenticationFilter jwtAuthFilter,
|
||||||
|
RateLimitFilter rateLimitFilter,
|
||||||
UserDetailsService userDetailsService,
|
UserDetailsService userDetailsService,
|
||||||
RestAuthenticationEntryPoint restAuthenticationEntryPoint,
|
RestAuthenticationEntryPoint restAuthenticationEntryPoint,
|
||||||
RestAccessDeniedHandler restAccessDeniedHandler) {
|
RestAccessDeniedHandler restAccessDeniedHandler) {
|
||||||
this.jwtAuthFilter = jwtAuthFilter;
|
this.jwtAuthFilter = jwtAuthFilter;
|
||||||
|
this.rateLimitFilter = rateLimitFilter;
|
||||||
this.userDetailsService = userDetailsService;
|
this.userDetailsService = userDetailsService;
|
||||||
this.restAuthenticationEntryPoint = restAuthenticationEntryPoint;
|
this.restAuthenticationEntryPoint = restAuthenticationEntryPoint;
|
||||||
this.restAccessDeniedHandler = restAccessDeniedHandler;
|
this.restAccessDeniedHandler = restAccessDeniedHandler;
|
||||||
@@ -75,6 +78,7 @@ public class SecurityConfig {
|
|||||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.authenticationProvider(daoAuthenticationProvider())
|
.authenticationProvider(daoAuthenticationProvider())
|
||||||
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
|
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
http.addFilterBefore(rateLimitFilter, JwtAuthenticationFilter.class);
|
||||||
http.addFilterAfter(activityLoggingFilter, JwtAuthenticationFilter.class);
|
http.addFilterAfter(activityLoggingFilter, JwtAuthenticationFilter.class);
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
|
|||||||
Reference in New Issue
Block a user