Skip to content

Commit 300252a

Browse files
committed
feat: add rate limit
1 parent 4d0682b commit 300252a

File tree

9 files changed

+136
-12
lines changed

9 files changed

+136
-12
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,9 @@ Creating database migration files
113113

114114
## Environment variables
115115

116-
| **Descrição** | **Parameter** | **Default values** |
116+
| **Description** | **Parameter** | **Default values** |
117117
|------------------------------------------|------------------------------------|---------------------------|
118-
| Server port | `SERVER_PORT` | 8080 |
118+
| server port | `SERVER_PORT` | 8080 |
119119
| database url | `DB_URL` | localhost:5432/common_app |
120120
| username (database) | `DB_USERNAME` | root |
121121
| user password (database) | `DB_PASSWORD` | root |
@@ -129,6 +129,7 @@ Creating database migration files
129129
| SMTP username | `SMTP_USERNAME` | user |
130130
| SMTP server password | `SMTP_PASSWORD` | secret |
131131
| time for recovery email to expire | `MINUTES_TO_EXPIRE_RECOVERY_CODE` | 20 |
132+
| max requests per minute | `MAX_REQUESTS_PER_MINUTE` | 10 |
132133

133134
> these variables are defined in: [**application.properties**](./src/main/resources/application.properties)
134135
>

pom.xml

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
</pluginRepositories>
2727

2828
<dependencies>
29+
<!-- spring boot -->
2930
<dependency>
3031
<groupId>org.springframework.boot</groupId>
3132
<artifactId>spring-boot-starter-data-jpa</artifactId>
@@ -50,6 +51,8 @@
5051
<groupId>org.springframework.boot</groupId>
5152
<artifactId>spring-boot-starter-thymeleaf</artifactId>
5253
</dependency>
54+
55+
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
5356
<dependency>
5457
<groupId>org.projectlombok</groupId>
5558
<artifactId>lombok</artifactId>
@@ -63,14 +66,12 @@
6366
<version>1.0.2</version>
6467
</dependency>
6568

66-
6769
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-data -->
6870
<dependency>
6971
<groupId>org.springframework.security</groupId>
7072
<artifactId>spring-security-data</artifactId>
7173
</dependency>
7274

73-
7475
<!-- flyway database | migrations -->
7576
<dependency>
7677
<groupId>org.flywaydb</groupId>
@@ -85,17 +86,17 @@
8586
<dependency>
8687
<groupId>org.springdoc</groupId>
8788
<artifactId>springdoc-openapi-ui</artifactId>
88-
<version>1.5.13</version>
89+
<version>1.6.6</version>
8990
</dependency>
9091
<dependency>
9192
<groupId>org.springdoc</groupId>
9293
<artifactId>springdoc-openapi-webmvc-core</artifactId>
93-
<version>1.5.13</version>
94+
<version>1.6.6</version>
9495
</dependency>
9596
<dependency>
9697
<groupId>org.springdoc</groupId>
9798
<artifactId>springdoc-openapi-security</artifactId>
98-
<version>1.5.13</version>
99+
<version>1.6.5</version>
99100
</dependency>
100101

101102
<!-- Token JWT -->
@@ -126,6 +127,7 @@
126127
<version>5.1.3</version>
127128
</dependency>
128129

130+
<!-- extras -->
129131
<dependency>
130132
<groupId>org.thymeleaf.extras</groupId>
131133
<artifactId>thymeleaf-extras-java8time</artifactId>
@@ -146,6 +148,7 @@
146148
<optional>true</optional>
147149
</dependency>
148150

151+
<!-- database -->
149152
<dependency>
150153
<groupId>com.h2database</groupId>
151154
<artifactId>h2</artifactId>
@@ -157,6 +160,21 @@
157160
<scope>runtime</scope>
158161
</dependency>
159162

163+
<!-- rate limit -->
164+
<dependency>
165+
<groupId>com.giffing.bucket4j.spring.boot.starter</groupId>
166+
<artifactId>bucket4j-spring-boot-starter</artifactId>
167+
<version>0.5.0</version>
168+
</dependency>
169+
<dependency>
170+
<groupId>org.springframework.boot</groupId>
171+
<artifactId>spring-boot-starter-cache</artifactId>
172+
</dependency>
173+
<dependency>
174+
<groupId>org.ehcache</groupId>
175+
<artifactId>ehcache</artifactId>
176+
</dependency>
177+
160178
<!-- tests -->
161179
<dependency>
162180
<groupId>org.springframework.boot</groupId>

src/main/java/com/github/throyer/common/springboot/Application.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.cache.annotation.EnableCaching;
56
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
67

8+
@EnableCaching
79
@SpringBootApplication
810
public class Application {
9-
1011
public static void main(String... args) {
1112
SpringApplication.run(Application.class, args);
1213
}

src/main/java/com/github/throyer/common/springboot/utils/Constants.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ public Constants(
1717
@Value("${token.secret}") String tokenSecret,
1818
@Value("${token.expiration-in-hours}") Integer tokenExpirationInHours,
1919
@Value("${token.refresh.expiration-in-days}") Integer refreshTokenExpirationInDays,
20-
@Value("${recovery.minutes-to-expire}") Integer recoveryMinutesToExpire
20+
@Value("${recovery.minutes-to-expire}") Integer recoveryMinutesToExpire,
21+
@Value("${bucket4j.filters[0].rate-limits[0].bandwidths[0].capacity}") Integer maxRequestsPerMinute
2122
) {
2223
Constants.SECURITY.TOKEN_SECRET = tokenSecret;
2324
Constants.SECURITY.TOKEN_EXPIRATION_IN_HOURS = tokenExpirationInHours;
2425
Constants.SECURITY.REFRESH_TOKEN_EXPIRATION_IN_DAYS = refreshTokenExpirationInDays;
2526

2627
Constants.MAIL.MINUTES_TO_EXPIRE_RECOVERY_CODE = recoveryMinutesToExpire;
28+
Constants.RATE_LIMIT.MAX_REQUESTS_PER_MINUTE = maxRequestsPerMinute;
2729
}
2830

2931
public static class SECURITY {
@@ -84,6 +86,10 @@ public static class MAIL {
8486
public static final String UNABLE_TO_SEND_EMAIL_MESSAGE_TEMPLATE = "Unable to send email to: %s";
8587
}
8688

89+
public static class RATE_LIMIT {
90+
public static Integer MAX_REQUESTS_PER_MINUTE;
91+
}
92+
8793
/**
8894
* Validation messages.
8995
* @see "resources/messages.properties"

src/main/resources/application.properties

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,23 @@ server.servlet.encoding.force-response=true
5959
# locale
6060
spring.web.locale=en
6161
spring.messages.encoding=UTF-8
62-
spring.messages.fallback-to-system-locale=false
62+
spring.messages.fallback-to-system-locale=false
63+
64+
# rate limit
65+
spring.cache.jcache.config=classpath:ehcache.xml
66+
bucket4j.enabled=true
67+
bucket4j.filters[0].cache-name=buckets
68+
bucket4j.filters[0].filter-method=servlet
69+
bucket4j.filters[0].http-response-body={ "status": 249, "message": "Too many requests" }
70+
bucket4j.filters[0].url=.*
71+
bucket4j.filters[0].metrics.enabled=true
72+
bucket4j.filters[0].metrics.tags[0].key=IP
73+
bucket4j.filters[0].metrics.tags[0].expression=getRemoteAddr()
74+
bucket4j.filters[0].strategy=first
75+
bucket4j.filters[0].rate-limits[0].skip-condition=getRequestURI().contains('/swagger-ui') || getRequestURI().contains('/documentation')
76+
bucket4j.filters[0].rate-limits[0].expression=getRemoteAddr()
77+
bucket4j.filters[0].rate-limits[0].bandwidths[0].capacity=${MAX_REQUESTS_PER_MINUTE:10}
78+
bucket4j.filters[0].rate-limits[0].bandwidths[0].time=1
79+
bucket4j.filters[0].rate-limits[0].bandwidths[0].unit=minutes
80+
bucket4j.filters[0].rate-limits[0].bandwidths[0].fixed-refill-interval=0
81+
bucket4j.filters[0].rate-limits[0].bandwidths[0].fixed-refill-interval-unit=minutes

src/main/resources/ehcache.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<config xmlns='http://www.ehcache.org/v3'
2+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xmlns:jsr107="http://www.ehcache.org/v3/jsr107"
4+
xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd
5+
http://www.ehcache.org/v3/jsr107 http://www.ehcache.org/schema/ehcache-107-ext-3.0.xsd">
6+
<cache alias="buckets">
7+
<expiry>
8+
<ttl unit="seconds">3600</ttl>
9+
</expiry>
10+
<heap unit="entries">1000000</heap>
11+
<jsr107:mbeans enable-statistics="true"/>
12+
</cache>
13+
14+
</config>
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.github.throyer.common.springboot.localization;
1+
package com.github.throyer.common.springboot;
22

33
import org.junit.jupiter.api.DisplayName;
44
import org.junit.jupiter.api.Test;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.github.throyer.common.springboot;
2+
3+
import org.junit.jupiter.api.DisplayName;
4+
import org.junit.jupiter.api.Test;
5+
import org.junit.jupiter.api.TestInstance;
6+
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
8+
import org.springframework.boot.test.context.SpringBootTest;
9+
import org.springframework.test.annotation.DirtiesContext;
10+
import org.springframework.test.web.servlet.MockMvc;
11+
12+
import static com.github.throyer.common.springboot.utils.Constants.RATE_LIMIT.MAX_REQUESTS_PER_MINUTE;
13+
import static java.lang.String.valueOf;
14+
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
15+
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.MOCK;
16+
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
17+
import static org.springframework.http.MediaType.APPLICATION_JSON;
18+
import static org.springframework.test.annotation.DirtiesContext.ClassMode.BEFORE_CLASS;
19+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
20+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
21+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
22+
23+
@AutoConfigureMockMvc
24+
@TestInstance(PER_CLASS)
25+
@SpringBootTest(webEnvironment = MOCK)
26+
@DirtiesContext(classMode = BEFORE_CLASS)
27+
public class RateLimitTests {
28+
@Autowired
29+
private MockMvc api;
30+
31+
@Test
32+
@DisplayName("Deve retornar TOO_MANY_REQUESTS quando a quantidade de requests passar do limite.")
33+
public void should_return_TOO_MANY_REQUESTS_when_number_of_requests_exceeds_the_limit() throws Exception {
34+
var request = get("/api")
35+
.header(CONTENT_TYPE, APPLICATION_JSON);
36+
37+
for (int index = MAX_REQUESTS_PER_MINUTE; index > 0; index--) {
38+
api.perform(request)
39+
.andExpect(header().string("X-Rate-Limit-Remaining", valueOf(index - 1)))
40+
.andExpect(status().isOk());
41+
}
42+
43+
api.perform(request)
44+
.andExpect(status().isTooManyRequests());
45+
}
46+
}

src/test/resources/application.properties

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,23 @@ server.servlet.encoding.force-response=true
4747
# locale
4848
spring.web.locale=en
4949
spring.messages.encoding=UTF-8
50-
spring.messages.fallback-to-system-locale=false
50+
spring.messages.fallback-to-system-locale=false
51+
52+
# rate limit
53+
spring.cache.jcache.config=classpath:ehcache.xml
54+
bucket4j.enabled=true
55+
bucket4j.filters[0].cache-name=buckets
56+
bucket4j.filters[0].filter-method=servlet
57+
bucket4j.filters[0].http-response-body={ "status": 249, "message": "Too many requests" }
58+
bucket4j.filters[0].url=/api
59+
bucket4j.filters[0].metrics.enabled=true
60+
bucket4j.filters[0].metrics.tags[0].key=IP
61+
bucket4j.filters[0].metrics.tags[0].expression=getRemoteAddr()
62+
bucket4j.filters[0].strategy=first
63+
bucket4j.filters[0].rate-limits[0].skip-condition=getRequestURI().contains('/swagger-ui') || getRequestURI().contains('/documentation')
64+
bucket4j.filters[0].rate-limits[0].expression=getRemoteAddr()
65+
bucket4j.filters[0].rate-limits[0].bandwidths[0].capacity=${MAX_REQUESTS_PER_MINUTE:10}
66+
bucket4j.filters[0].rate-limits[0].bandwidths[0].time=1
67+
bucket4j.filters[0].rate-limits[0].bandwidths[0].unit=minutes
68+
bucket4j.filters[0].rate-limits[0].bandwidths[0].fixed-refill-interval=0
69+
bucket4j.filters[0].rate-limits[0].bandwidths[0].fixed-refill-interval-unit=minutes

0 commit comments

Comments
 (0)