1

I am trying to implement Bucket4J rate limiter in Netflix Zuul Api Gateway. I have added Interceptor for Rate Limiting the requests using WebMvcConfigurer.

package com.rajkumar.apiigateway;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.rajkumar.apiigateway.ratelimit.interceptor.RateLimitInterceptor;

@SpringBootApplication
@EnableZuulProxy
public class ApiiGatewayApplication implements WebMvcConfigurer{

    @Autowired
    @Lazy
    RateLimitInterceptor rateLimitInterceptor;
    

    
    public static void main(String[] args) {
        new SpringApplicationBuilder(ApiiGatewayApplication.class)
                    .run(args);
    }
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(rateLimitInterceptor)
        .addPathPatterns("/api/service_1/throttling/users");
    }

}

And Interceptor for rate limiting looks like

package com.rajkumar.apiigateway.ratelimit.interceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import com.rajkumar.apiigateway.ratelimit.service.RateLimitService;

import io.github.bucket4j.Bucket;
import io.github.bucket4j.ConsumptionProbe;

@Component
public class RateLimitInterceptor implements HandlerInterceptor {
    private static final String HEADER_API_KEY = "X-API-KEY";
    private static final String HEADER_LIMIT_REMAINING = "X-RATE-LIMIT-REMAINING";
    private static final String HEADER_RETRY_AFTER = "X-RATE-LIMIT-RETRY-AFTER-SECONDS";

    
    @Autowired
    RateLimitService rateLimitService;
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        String apiKey = request.getHeader(HEADER_API_KEY);
        if(apiKey == null || apiKey.isEmpty()) {
            response.sendError(HttpStatus.OK.value(),  HEADER_API_KEY+" request header is mandatory");
            return false;
        }
        
        Bucket tokenBucket = rateLimitService.resolveBucket(request.getHeader(HEADER_API_KEY));
        ConsumptionProbe probe = tokenBucket.tryConsumeAndReturnRemaining(1);
        
        if(probe.isConsumed()) {
            response.addHeader(HEADER_LIMIT_REMAINING, Long.toString(probe.getRemainingTokens()));
            return true;
        }       
        response.addHeader(HEADER_RETRY_AFTER, Long.toString(probe.getNanosToWaitForRefill()/1000000000));
        response.sendError(HttpStatus.TOO_MANY_REQUESTS.value(),"You have exceeded your request limit");
        return false;
    }

}

and other dependent component

package com.rajkumar.apiigateway.ratelimit.service;

import java.util.concurrent.TimeUnit;

import org.springframework.stereotype.Component;

import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.rajkumar.apiigateway.ratelimit.RateLimit;

import io.github.bucket4j.Bucket;
import io.github.bucket4j.Bucket4j;

@Component
public class RateLimitService {
private LoadingCache<String, Bucket> cache = Caffeine.newBuilder()
                                                    .expireAfterWrite(1, TimeUnit.MINUTES)
                                                        .build(this::newBucket);
    
    public Bucket resolveBucket(String apiKey) {
        return cache.get(apiKey);
    }
    
    private Bucket newBucket(String apiKey) {
        RateLimit plan = RateLimit.resolvePlanFromApiKey(apiKey);
        Bucket bucket = Bucket4j.builder()  
                        .addLimit(plan.getLimit())
                        .build();
        return bucket;
    }
}

package com.rajkumar.apiigateway.ratelimit;

import java.time.Duration;

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Refill;

public enum RateLimit {

    FREE(2L),
    BASIC(4L),
    PROFESSIONAL(10L);
    
    private Long tokens;
    
    private RateLimit(Long tokens) {
        this.tokens = tokens;
    }
     public static RateLimit resolvePlanFromApiKey(String apiKey) {
        if(apiKey==null || apiKey.isEmpty()) {
            return FREE;
        }
        else if(apiKey.startsWith("BAS-")) {
            return BASIC;
        }
        else if(apiKey.startsWith("PRO-")) {
            return PROFESSIONAL;
        }
        return FREE;
    }
    
     public Bandwidth getLimit() {
            return Bandwidth.classic(tokens, Refill.intervally(tokens, Duration.ofMinutes(1)));
     }
}

and pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.rajkumar</groupId>
    <artifactId>apii-gateway</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>apii-gateway</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR8</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
            <!-- <version>2.5.5</version> -->
        </dependency>
        <dependency>
            <groupId>com.github.vladimir-bukhtoyarov</groupId>
            <artifactId>bucket4j-core</artifactId>
            <version>4.10.0</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

and application.properties

server.port = 8080

spring.application.name = api-gateway

#routing for service 1
zuul.routes.service_1.path = /api/service_1/**
zuul.routes.service_1.url = http://localhost:8081/


#routing for service 2
zuul.routes.service_2.path = /api/service_2/**
zuul.routes.service_2.url = http://localhost:8082/

When I am trying to hit api gateway (http://localhost:8080/api/service_1/throttling/users): it is not passing through the interceptor. Any help is appreciated. Thanks in advance.

Rajkumar Alagar
  • 111
  • 1
  • 7
  • Hi Rajkumar, what do you mean by not passing through the interceptor? You mean it's not being intercepted or it's throwing an error? – Marcos Barbero Nov 02 '20 at 09:01
  • Hi Marcos, It's not being intercepted. – Rajkumar Alagar Nov 02 '20 at 09:30
  • Have you tried extracting the HandlerInterceptor into a @Configuration class, not in main spring app, see this link: https://stackoverflow.com/a/45585071/3942132 because I am not sure how the bean will loaded if you put it in the main spring boot app – Roie Beck Nov 02 '20 at 11:57
  • I think your problem is happening because Zuul runs it's own ZuulServlet and doesn't pick up what you have added. Anyway, this answer here explains it a little further https://stackoverflow.com/questions/39801282/handlerinterceptoradapter-and-zuul-filter?answertab=active#tab-top – Marcos Barbero Nov 02 '20 at 12:05
  • Thank you @MarcosBarbero. It is now Working with the solution that is present the given reference. – Rajkumar Alagar Nov 03 '20 at 06:51

0 Answers0