Skip to content

Commit d819612

Browse files
committed
Implements rate limiting for API protection
Adds rate limiting functionality using Bucket4j to protect the API from abuse. Introduces a filter to limit the number of requests a user can make within a timeframe. Implements a custom exception and handler to return informative error messages when the rate limit is exceeded. Updates logging to include retry information for rate limit exceptions.
1 parent 4b8d929 commit d819612

File tree

11 files changed

+139
-14
lines changed

11 files changed

+139
-14
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ build/
3838

3939
### Logs ###
4040
logs/
41-
*.log
41+
*.log
42+
*.sh

docker-compose.prod.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,9 @@ services:
1212
- SPRING_DATA_MONGODB_DATABASE=${MONGO_DATABASE}
1313
- SPRING_DATA_MONGODB_USERNAME=${MONGO_USERNAME}
1414
- SPRING_DATA_MONGODB_PASSWORD=${MONGO_PASSWORD}
15+
logging:
16+
driver: "json-file"
17+
options:
18+
max-size: "10m"
19+
max-file: "3"
1520
restart: unless-stopped

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@
8989
<artifactId>logstash-logback-encoder</artifactId>
9090
<version>9.0</version>
9191
</dependency>
92+
<dependency>
93+
<groupId>com.bucket4j</groupId>
94+
<artifactId>bucket4j-core</artifactId>
95+
<version>8.10.1</version>
96+
</dependency>
9297
</dependencies>
9398

9499
<build>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.dmware.api_onibusbh.exceptions;
2+
3+
public class RateLimitExceededException extends RuntimeException {
4+
5+
private final long retryAfterSeconds;
6+
7+
public RateLimitExceededException(String message, long retryAfterSeconds) {
8+
super(message);
9+
this.retryAfterSeconds = retryAfterSeconds;
10+
}
11+
12+
public long getRetryAfterSeconds() {
13+
return retryAfterSeconds;
14+
}
15+
}

src/main/java/com/dmware/api_onibusbh/infra/CustomExceptionHandler.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,12 @@ public ResponseEntity<ErrorResponse> noHandlerFoundException(NoHandlerFoundExcep
7575
logger.warn("Recurso não encontrado", kv("exception", ex.getClass().getSimpleName()), kv("status", HttpStatus.NOT_FOUND));
7676
return ErrorResponse.of("Verifique a rota digitada ou os dados enviados", HttpStatus.NOT_FOUND);
7777
}
78+
79+
@ExceptionHandler(RateLimitExceededException.class)
80+
public ResponseEntity<ErrorResponse> rateLimitExceededException(RateLimitExceededException ex) {
81+
logger.warn("Rate limit excedido", kv("exception", ex.getClass().getSimpleName()), kv("status", HttpStatus.TOO_MANY_REQUESTS), kv("retry_after", ex.getRetryAfterSeconds()));
82+
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
83+
.header("X-Rate-Limit-Retry-After-Seconds", String.valueOf(ex.getRetryAfterSeconds()))
84+
.body(new ErrorResponse(java.time.LocalDateTime.now(), ex.getMessage(), HttpStatus.TOO_MANY_REQUESTS));
85+
}
7886
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.dmware.api_onibusbh.infra;
2+
3+
import com.dmware.api_onibusbh.exceptions.RateLimitExceededException;
4+
import io.github.bucket4j.Bucket;
5+
import io.github.bucket4j.ConsumptionProbe;
6+
import jakarta.servlet.FilterChain;
7+
import jakarta.servlet.ServletException;
8+
import jakarta.servlet.http.HttpServletRequest;
9+
import jakarta.servlet.http.HttpServletResponse;
10+
import org.springframework.beans.factory.annotation.Autowired;
11+
import org.springframework.beans.factory.annotation.Value;
12+
import org.springframework.stereotype.Component;
13+
import org.springframework.web.filter.OncePerRequestFilter;
14+
import org.springframework.web.servlet.HandlerExceptionResolver;
15+
16+
import java.io.IOException;
17+
import java.time.Duration;
18+
import java.util.Map;
19+
import java.util.concurrent.ConcurrentHashMap;
20+
import java.util.concurrent.TimeUnit;
21+
22+
@Component
23+
public class RateLimitingFilter extends OncePerRequestFilter {
24+
25+
private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();
26+
27+
@Autowired
28+
private HandlerExceptionResolver handlerExceptionResolver;
29+
30+
@Value("${rate.limit.capacity:60}")
31+
private long capacity;
32+
33+
@Value("${rate.limit.tokens:60}")
34+
private long tokens;
35+
36+
@Value("${rate.limit.duration-seconds:60}")
37+
private long durationSeconds;
38+
39+
@Override
40+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
41+
throws ServletException, IOException {
42+
43+
String clientIp = request.getRemoteAddr();
44+
Bucket bucket = buckets.computeIfAbsent(clientIp, this::createNewBucket);
45+
46+
ConsumptionProbe probe = bucket.tryConsumeAndReturnRemaining(1);
47+
48+
if (probe.isConsumed()) {
49+
response.setHeader("X-Rate-Limit-Remaining", String.valueOf(probe.getRemainingTokens()));
50+
filterChain.doFilter(request, response);
51+
} else {
52+
long waitForRefill = probe.getNanosToWaitForRefill();
53+
long retryAfterSeconds = TimeUnit.NANOSECONDS.toSeconds(waitForRefill);
54+
55+
handlerExceptionResolver.resolveException(request, response, null,
56+
new RateLimitExceededException(
57+
"Rate limit exceeded. Try again in " + retryAfterSeconds + " seconds.",
58+
retryAfterSeconds
59+
));
60+
}
61+
}
62+
63+
private Bucket createNewBucket(String key) {
64+
return Bucket.builder()
65+
.addLimit(limit -> limit.capacity(capacity).refillGreedy(tokens, Duration.ofSeconds(durationSeconds)))
66+
.build();
67+
}
68+
}

src/main/java/com/dmware/api_onibusbh/services/APIService.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ public List<LinhaDTO> getLinhasAPIBH() {
6969
}
7070

7171
public List<CoordenadaDTO> getOnibusCoordenadaBH() {
72-
logger.info("Acessando diretamente.");
7372
List<String> responses = fetchCoordenadasDirectly();
7473

7574
if (responses == null || responses.size() < 2) {

src/main/java/com/dmware/api_onibusbh/services/OnibusService.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
package com.dmware.api_onibusbh.services;
22

3-
import com.dmware.api_onibusbh.config.WebClientConfig;
43
import com.dmware.api_onibusbh.dto.CoordenadaDTO;
54
import com.dmware.api_onibusbh.dto.OnibusDTO;
65
import com.dmware.api_onibusbh.entities.LinhaEntity;
76
import com.dmware.api_onibusbh.exceptions.CoordenadasNotFoundException;
87
import com.dmware.api_onibusbh.exceptions.LinhaNotFoundException;
98
import com.dmware.api_onibusbh.repositories.LinhasRepository;
10-
import com.fasterxml.jackson.databind.ObjectMapper;
119
import org.modelmapper.ModelMapper;
1210
import org.slf4j.Logger;
1311
import org.slf4j.LoggerFactory;
@@ -33,12 +31,10 @@ public class OnibusService {
3331

3432
private final LinhasRepository linhasRepository;
3533
private final ModelMapper modelMapper;
36-
private final ObjectMapper objectMapper;
3734

38-
public OnibusService(LinhasService linhasService, WebClientConfig webClientConfig, LinhasRepository linhasRepository, ModelMapper modelMapper, ObjectMapper objectMapper) {
35+
public OnibusService(LinhasRepository linhasRepository, ModelMapper modelMapper) {
3936
this.linhasRepository = linhasRepository;
4037
this.modelMapper = modelMapper;
41-
this.objectMapper = objectMapper;
4238
}
4339

4440

src/main/resources/application-prod.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ spring.data.mongodb.username=${MONGO_USERNAME}
66
spring.data.mongodb.password=${MONGO_PASSWORD}
77

88
logging.level.root=INFO
9-
logging.level.com.dmware.api_onibusbh=DEBUG
10-
logging.level.org.springframework.web=DEBUG
9+
logging.level.com.dmware.api_onibusbh=INFO
10+
logging.level.org.springframework.web=INFO
1111

1212
server.forward-headers-strategy=framework

src/main/resources/application.properties

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,12 @@ springdoc.swagger-ui.path=/swagger-ui.html
66
spring.data.mongodb.auto-index-creation=true
77
spring.data.mongodb.authentication-database=admin
88
# Business
9-
api.onibus.coordenadas.ttl-minutes=20
9+
api.onibus.coordenadas.ttl-minutes=20
10+
11+
# Rate Limiting (Bucket4j)
12+
rate.limit.capacity=60
13+
rate.limit.tokens=60
14+
rate.limit.duration-seconds=60
15+
16+
# Performance
17+
spring.threads.virtual.enabled=true

0 commit comments

Comments
 (0)