spring mvc using interceptor
This commit is contained in:
parent
e8c383d10f
commit
4e45b8f44e
|
@ -0,0 +1,29 @@
|
|||
package com.baeldung.ratelimiting.bucket4japp;
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import com.baeldung.ratelimiting.bucket4japp.interceptor.RateLimitingInterceptor;
|
||||
|
||||
@SpringBootApplication(scanBasePackages = "com.baeldung.ratelimiting", exclude = {
|
||||
DataSourceAutoConfiguration.class,
|
||||
SecurityAutoConfiguration.class
|
||||
})
|
||||
public class Bucket4jRateLimitingApp implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(new RateLimitingInterceptor())
|
||||
.addPathPatterns("/api/v1/area/**");
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
new SpringApplicationBuilder(Bucket4jRateLimitingApp.class)
|
||||
.properties("spring.config.location=classpath:ratelimiting/application-bucket4j.yml")
|
||||
.run(args);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package com.baeldung.ratelimiting.bucket4japp.interceptor;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import io.github.bucket4j.Bandwidth;
|
||||
import io.github.bucket4j.Refill;
|
||||
|
||||
enum PricingPlan {
|
||||
|
||||
FREE {
|
||||
|
||||
@Override
|
||||
Bandwidth getLimit() {
|
||||
return Bandwidth.classic(20, Refill.intervally(20, Duration.ofHours(1)));
|
||||
}
|
||||
},
|
||||
|
||||
BASIC {
|
||||
|
||||
@Override
|
||||
Bandwidth getLimit() {
|
||||
return Bandwidth.classic(40, Refill.intervally(40, Duration.ofHours(1)));
|
||||
}
|
||||
},
|
||||
|
||||
PROFESSIONAL {
|
||||
|
||||
@Override
|
||||
Bandwidth getLimit() {
|
||||
return Bandwidth.classic(100, Refill.intervally(100, Duration.ofHours(1)));
|
||||
}
|
||||
};
|
||||
|
||||
abstract Bandwidth getLimit();
|
||||
|
||||
static PricingPlan resolvePlanFromApiKey(String apiKey) {
|
||||
if (apiKey == null || apiKey.isEmpty()) {
|
||||
return FREE;
|
||||
|
||||
} else if (apiKey.startsWith("PX001-")) {
|
||||
return PROFESSIONAL;
|
||||
|
||||
} else if (apiKey.startsWith("BX001-")) {
|
||||
return BASIC;
|
||||
}
|
||||
return FREE;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package com.baeldung.ratelimiting.bucket4japp.interceptor;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import io.github.bucket4j.Bandwidth;
|
||||
import io.github.bucket4j.Bucket;
|
||||
import io.github.bucket4j.Bucket4j;
|
||||
import io.github.bucket4j.ConsumptionProbe;
|
||||
|
||||
public class RateLimitingInterceptor 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-Milliseconds";
|
||||
|
||||
private final Map<String, Bucket> cache = new ConcurrentHashMap<>();
|
||||
|
||||
@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.BAD_REQUEST.value(), "Missing Header: " + HEADER_API_KEY);
|
||||
return false;
|
||||
}
|
||||
|
||||
Bucket tokenBucket = cache.computeIfAbsent(apiKey, this::resolveBucket);
|
||||
|
||||
ConsumptionProbe probe = tokenBucket.tryConsumeAndReturnRemaining(1);
|
||||
|
||||
if (probe.isConsumed()) {
|
||||
|
||||
response.addHeader(HEADER_LIMIT_REMAINING, String.valueOf(probe.getRemainingTokens()));
|
||||
return true;
|
||||
|
||||
} else {
|
||||
|
||||
long waitForRefillMilli = probe.getNanosToWaitForRefill() % 1_000_000;
|
||||
|
||||
response.sendError(HttpStatus.TOO_MANY_REQUESTS.value(), "You have exhausted your API Request Quota"); // 429
|
||||
response.addHeader(HEADER_RETRY_AFTER, String.valueOf(waitForRefillMilli));
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Bucket resolveBucket(String apiKey) {
|
||||
PricingPlan pricingPlan = PricingPlan.resolvePlanFromApiKey(apiKey);
|
||||
return bucket(pricingPlan.getLimit());
|
||||
}
|
||||
|
||||
private Bucket bucket(Bandwidth limit) {
|
||||
return Bucket4j.builder()
|
||||
.addLimit(limit)
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
server:
|
||||
port: 9000
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: bucket4j-api-rate-limiting-app
|
||||
mvc:
|
||||
throw-exception-if-no-handler-found: true
|
||||
resources:
|
||||
add-mappings: false
|
Loading…
Reference in New Issue