Spring boot rate limit


Having your back end server serving to an unknown number of requests can be like opening a can of worms in today's world with the application being open to attacks such as DDOS and similar. In Spring boot there could be several ways to handle this and the two most common ways that one can think of can be by either via a @Component or a @Bean implementing a Filter
in both the cases. Let us see how this is done in both the ways.
Most importantly a @Service, @Repository, @Controller, @RestController are all nothing but a form of a @Component. Also a @Component can be called via a method as a @Bean in the main Spring Application class.
One can make use of the thread safe ConcurrentHashMap
to hold the ip address as String of the caller and the count using a thread safe AtomicInteger. The doFilter
method is what needs to be overridden of the Filter class, letting the call continue via the filterChain.doFilter(req, res)
if the calls from an ip was within the range.
Filter can be customized for a single or for all or for a range of end points by type casting the ServletRequest
to HttpServletRequest
and see if the URI matches with the required end points and given the pass or not.
In Spring server, there are many servlets running and each request passed through some and can be handled accordingly via Filter. The more Filter implementation, the more phases the request goes through.
Implementing the Filter interface via as a @Component.
package appName;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.lang.invoke.MethodHandles;
import java.util.logging.Level;
import java.util.logging.Logger;
// Either this annot or @Bean in app class
@Component
public class RateLimitingFilter implements Filter {
// Map to store request counts per IP address
private final Map<String, AtomicInteger> requestCountsPerIpAddress = new ConcurrentHashMap<>();
// Maximum requests allowed per minute
private static final int MAX_REQUESTS_PER_MINUTE = 5;
private static final Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass().getName());
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
/*
// use if using @Comp for this
// filter only for not this path
if(!httpServletRequest.getRequestURI().endsWith("/user/login")){
// Allow the request to proceed
chain.doFilter(request, response);
return;
}
*/
String clientIpAddress = httpServletRequest.getRemoteAddr();
// Initialize request count for the client IP address
requestCountsPerIpAddress.putIfAbsent(clientIpAddress, new AtomicInteger(0));
AtomicInteger requestCount = requestCountsPerIpAddress.get(clientIpAddress);
// Increment the request count
int requests = requestCount.incrementAndGet();
// logger.log(Level.WARNING, "Request is {0}", httpServletRequest.getRequestURI());
// Check if the request limit has been exceeded
if (requests > MAX_REQUESTS_PER_MINUTE) {
httpServletResponse.setStatus(429);
httpServletResponse.getWriter().write("Too many requests. Please try again later.");
return;
}
// Allow the request to proceed
chain.doFilter(request, response);
// Optional: Reset request counts periodically (not implemented in this simple example)
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Optional: Initialization logic, if needed
}
@Override
public void destroy() {
// TODO Auto-generated method stub
// throw new UnsupportedOperationException("Unimplemented method 'destroy'");
}
}
This will apply the filter for all, uncomment the block and/ or update to make it work for relevant endpoints if needed. This should be it and no further configuration is required. This way even if the Application class is not editable, the Filter can be implemented. Also I have hard coded the limit as a final variable, which can be updated the application properties file and accessed via the Environment getProperty .
If the Application class is editable, the @Component in the above implementation can be commented or removed and the @Bean be implemented like below.
@Bean
public FilterRegistrationBean<RateLimitingFilter> rateLimitingFilter() {
FilterRegistrationBean<RateLimitingFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new RateLimitingFilter());
registrationBean.addUrlPatterns("/user/login"); // Register filter for API endpoints
return registrationBean;
}
A FilterRegistrationBean of type RateLimitingFilter is created and returned. The URL patterns can be added here.
We have gotten this far, let us add the respective test as well.
@Test
@WithMockUser(username = "esuez5", authorities = {"SCOPE_stop:read"})
void zmaxFiveReqInAMin() throws Exception {
int count = 0;
// Assert.isTrue(this.mvcs.length == 2, "Length is not 2 but " + this.mvcs.length);
do {
this.mvc.perform(get("/user/login"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(2))
.andExpect(jsonPath("$.[0].stopCode").value(50001))
.andExpect(jsonPath("$.[1].stopCode").value(50011));
} while(++count < 5);
this.mvc.perform(get("/stops"))
.andExpect(status().isTooManyRequests());
}
This should conclude the post. All in all it is really impressive how the OOP concepts have been utilized to achieve rate limiting and other features.