Home ...

Spring boot rate limit

Ragavendra B.N.
Ragavendra B.N.

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.