From 40d9f6da930da9c9e5651353c73f03ee9ac5d692 Mon Sep 17 00:00:00 2001
From: Agit Rubar Demir <61833677+agitrubard@users.noreply.github.com>
Date: Sun, 22 Sep 2024 11:49:30 +0300
Subject: [PATCH] AYS-379 | Rate Limit Feature Has Been Integrated (#380)
---
.github/dependabot.yml | 11 --
pom.xml | 22 ++++
.../AysBearerTokenAuthenticationFilter.java | 105 +++++++++++++++++-
.../common/util/HttpServletRequestUtil.java | 19 ++++
src/main/resources/application.yml | 5 +
5 files changed, 148 insertions(+), 14 deletions(-)
delete mode 100644 .github/dependabot.yml
create mode 100644 src/main/java/org/ays/common/util/HttpServletRequestUtil.java
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index ac6621f19..000000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-# To get started with Dependabot version updates, you'll need to specify which
-# package ecosystems to update and where the package manifests are located.
-# Please see the documentation for all configuration options:
-# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
-
-version: 2
-updates:
- - package-ecosystem: "" # See documentation for possible values
- directory: "/" # Location of package manifests
- schedule:
- interval: "weekly"
diff --git a/pom.xml b/pom.xml
index ccd0e6950..b07b62a31 100644
--- a/pom.xml
+++ b/pom.xml
@@ -39,6 +39,9 @@
0.12.6
1.78.1
+ 8.10.1
+ 33.3.0-jre
+
3.3
1.27.0
1.12.0
@@ -119,6 +122,25 @@
+
+
+
+
+ com.bucket4j
+ bucket4j-core
+ ${bucket4j-core.version}
+
+
+
+ com.google.guava
+ guava
+ ${guava.version}
+
+
+
+
+
+
diff --git a/src/main/java/org/ays/auth/filter/AysBearerTokenAuthenticationFilter.java b/src/main/java/org/ays/auth/filter/AysBearerTokenAuthenticationFilter.java
index 08afb232e..afa3b1613 100644
--- a/src/main/java/org/ays/auth/filter/AysBearerTokenAuthenticationFilter.java
+++ b/src/main/java/org/ays/auth/filter/AysBearerTokenAuthenticationFilter.java
@@ -1,14 +1,22 @@
package org.ays.auth.filter;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import io.github.bucket4j.Bandwidth;
+import io.github.bucket4j.Bucket;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.ays.auth.model.AysToken;
import org.ays.auth.service.AysInvalidTokenService;
import org.ays.auth.service.AysTokenService;
+import org.ays.common.util.HttpServletRequestUtil;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.lang.NonNull;
import org.springframework.security.core.context.SecurityContextHolder;
@@ -16,6 +24,9 @@
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
+import java.time.Duration;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
/**
* AysBearerTokenAuthenticationFilter is a filter that intercepts HTTP requests and processes the Bearer tokens included in the Authorization headers.
@@ -32,13 +43,50 @@ public class AysBearerTokenAuthenticationFilter extends OncePerRequestFilter {
private final AysInvalidTokenService invalidTokenService;
+ @Value("${ays.rate-limit.authorized.enabled}")
+ private boolean isAuthorizedRateLimitEnabled;
+
+ private static final int MAXIMUM_AUTHORIZED_REQUESTS_COUNTS = 20;
+ private static final int MAXIMUM_AUTHORIZED_REQUESTS_DURATION_MINUTES = 1;
+ private static final Duration MAXIMUM_AUTHORIZED_REQUESTS_DURATION = Duration.ofMinutes(MAXIMUM_AUTHORIZED_REQUESTS_DURATION_MINUTES);
+ private final LoadingCache authorizedBuckets = CacheBuilder.newBuilder()
+ .expireAfterWrite(MAXIMUM_AUTHORIZED_REQUESTS_DURATION_MINUTES, TimeUnit.MINUTES)
+ .build(new CacheLoader<>() {
+ @Override
+ public @NotNull Bucket load(@NotNull String key) {
+ return newBucket(
+ MAXIMUM_AUTHORIZED_REQUESTS_COUNTS,
+ MAXIMUM_AUTHORIZED_REQUESTS_DURATION
+ );
+ }
+ });
+
+
+ @Value("${ays.rate-limit.unauthorized.enabled}")
+ private boolean isUnauthorizedRateLimitEnabled;
+
+ private static final int MAXIMUM_UNAUTHORIZED_REQUESTS_COUNTS = 5;
+ private static final int MAXIMUM_UNAUTHORIZED_REQUESTS_DURATION_MINUTES = 10;
+ private static final Duration MAXIMUM_UNAUTHORIZED_REQUESTS_DURATION = Duration.ofMinutes(MAXIMUM_UNAUTHORIZED_REQUESTS_DURATION_MINUTES);
+ private final LoadingCache unauthorizedBuckets = CacheBuilder.newBuilder()
+ .expireAfterWrite(MAXIMUM_UNAUTHORIZED_REQUESTS_DURATION_MINUTES, TimeUnit.MINUTES)
+ .build(new CacheLoader<>() {
+ @Override
+ public @NotNull Bucket load(@NotNull String key) {
+ return newBucket(
+ MAXIMUM_UNAUTHORIZED_REQUESTS_COUNTS,
+ MAXIMUM_UNAUTHORIZED_REQUESTS_DURATION
+ );
+ }
+ });
+
+
@Override
- protected void doFilterInternal(HttpServletRequest httpServletRequest,
+ protected void doFilterInternal(@NotNull HttpServletRequest httpServletRequest,
@NonNull HttpServletResponse httpServletResponse,
@NonNull FilterChain filterChain) throws ServletException, IOException {
-
- log.debug("API Request was secured with AYS Security!");
+ final String ipAddress = HttpServletRequestUtil.getClientIpAddress(httpServletRequest);
final String authorizationHeader = httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION);
if (AysToken.isBearerToken(authorizationHeader)) {
@@ -49,11 +97,62 @@ protected void doFilterInternal(HttpServletRequest httpServletRequest,
final String tokenId = tokenService.getPayload(jwt).getId();
invalidTokenService.checkForInvalidityOfToken(tokenId);
+ if (isAuthorizedRateLimitEnabled) {
+ boolean isRateLimitExceeded = this.isRateLimitExceeded(ipAddress, authorizedBuckets, httpServletResponse);
+ if (isRateLimitExceeded) {
+ return;
+ }
+ }
+
final var authentication = tokenService.getAuthentication(jwt);
SecurityContextHolder.getContext().setAuthentication(authentication);
+ filterChain.doFilter(httpServletRequest, httpServletResponse);
+ return;
+ }
+
+
+ if (isUnauthorizedRateLimitEnabled) {
+ boolean isRateLimitExceeded = this.isRateLimitExceeded(ipAddress, unauthorizedBuckets, httpServletResponse);
+ if (isRateLimitExceeded) {
+ return;
+ }
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
+
+ }
+
+ private boolean isRateLimitExceeded(final String ipAddress,
+ final LoadingCache buckets,
+ final HttpServletResponse httpServletResponse) {
+
+ try {
+
+ final Bucket bucket = buckets.get(ipAddress);
+ if (bucket.tryConsume(1)) {
+ return false;
+ }
+
+ } catch (ExecutionException exception) {
+ log.error("Error while checking rate limit for IP: {}", ipAddress, exception);
+ httpServletResponse.setStatus(429);
+ return true;
+ }
+
+ log.warn("Rate limit exceeded for IP: {}", ipAddress);
+ httpServletResponse.setStatus(429);
+ return true;
+ }
+
+ private static Bucket newBucket(int maximumRequestsCounts, Duration maximumDuration) {
+ final Bandwidth bandwidth = Bandwidth
+ .builder()
+ .capacity(maximumRequestsCounts)
+ .refillIntervally(maximumRequestsCounts, maximumDuration)
+ .build();
+ return Bucket.builder()
+ .addLimit(bandwidth)
+ .build();
}
}
diff --git a/src/main/java/org/ays/common/util/HttpServletRequestUtil.java b/src/main/java/org/ays/common/util/HttpServletRequestUtil.java
new file mode 100644
index 000000000..44bffbe6d
--- /dev/null
+++ b/src/main/java/org/ays/common/util/HttpServletRequestUtil.java
@@ -0,0 +1,19 @@
+package org.ays.common.util;
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.experimental.UtilityClass;
+
+@UtilityClass
+public class HttpServletRequestUtil {
+
+ public static String getClientIpAddress(final HttpServletRequest httpServletRequest) {
+
+ final String ipAddress = httpServletRequest.getHeader("X-Forwarded-For");
+ if (ipAddress == null || ipAddress.isEmpty()) {
+ return httpServletRequest.getRemoteAddr().trim();
+ }
+
+ return ipAddress.split(",")[0].trim();
+ }
+
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index f9ac8e885..d4eaad718 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -81,3 +81,8 @@ ays:
invalid-tokens-deletion:
cron: ${INVALID_TOKENS_DELETION_CRON:0 0 */3 * * ?}
enable: ${INVALID_TOKENS_DELETION_ENABLED:true}
+ rate-limit:
+ authorized:
+ enabled: ${AYS_AUTHORIZED_RATE_LIMIT_ENABLED:false}
+ unauthorized:
+ enabled: ${AYS_UNAUTHORIZED_RATE_LIMIT_ENABLED:false}