spring boot 프로젝트 생성하면 main 메소드가 생성되는 위치가 있는데,
해당 위치에 모든 설정을 그냥 다 때려박았습니다.
package me.dailycode.hacktest;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import lombok.Builder;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.filters.RateLimitFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
@SpringBootApplication
public class HackTestApplication {
public static void main(String[] args) {
SpringApplication.run(HackTestApplication.class, args);
}
}
@Configuration
class FilterConfiguration {
@Bean
public FilterRegistrationBean<CustomRateLimitFilter> customRateLimitFilterRegistration() {
FilterRegistrationBean<CustomRateLimitFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new CustomRateLimitFilter());
registration.setName("customRateLimitFilter");
// /simple/* 이라는 url 로 오는 요청에 대하여 Filter 가 동작한다.
registration.addUrlPatterns("/simple/*");
// 2초 내로 50~51 건 이상의 요청이 오면 막힌다!
registration.addInitParameter("bucketDuration", "2");
registration.addInitParameter("bucketRequests", "50");
// 사실 지금은 필요 X, 여러 Filter 가 있을 때 order 를 써주면 좋다.
registration.setOrder(1);
// 요청에 대해서만 반응한다.
registration.setDispatcherTypes(DispatcherType.REQUEST);
return registration;
}
}
@Slf4j
class CustomRateLimitFilter extends RateLimitFilter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest servletRequest) {
// POST, PUT, DELETE 요청에 대해서만 동작한다.
if("POST".equalsIgnoreCase(servletRequest.getMethod()) ||
"PUT".equalsIgnoreCase(servletRequest.getMethod()) ||
"DELETE".equalsIgnoreCase(servletRequest.getMethod()))
{
super.doFilter(request, response, chain);
return;
}
}
chain.doFilter(request, response);
}
}
package me.dailycode.hacktest.controller;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import lombok.Builder;
import me.dailycode.hacktest.*;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
@Controller
@RequestMapping("/simple")
public class SimpleController {
record SimpleGetRequestDTO(String id, String name, Integer age) {}
@Builder
record SimpleGetResponseDTO(String id, String name, Integer age) {}
record SimplePostRequestDTO(@NotEmpty String name, @Min(0) @Max(120) Integer age) {}
@Builder
record SimplePostResponseDTO(String name, Integer age) {}
@GetMapping("/{id}")
public ResponseEntity<SimpleGetResponseDTO> getMethod(SimpleGetRequestDTO dto) {
return
ResponseEntity.ok(
SimpleGetResponseDTO.builder()
.id(dto.id())
.name("hello-world")
.age(100).build());
}
@PostMapping
public ResponseEntity<SimplePostResponseDTO> postMethod(@Valid SimplePostRequestDTO requestDTO) {
System.out.println("requestDTO = " + requestDTO);
return ResponseEntity.ok(
SimplePostResponseDTO.builder()
.name(requestDTO.name())
.age(requestDTO.age()).build()
);
}
}
package me.dailycode.hacktest.repeater;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
public class RepeatSendRequestTest {
@Test
void sendPostRequest() throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(100);
List<Thread> threadList = new ArrayList<>(100);
for (int i = 0; i < 100; i++) {
final int I = i;
Thread vThread = Thread.ofVirtual()
.unstarted(() -> {
HttpResponse<String> response;
try (HttpClient httpClient = HttpClient.newHttpClient()) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8081/simple"))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString("name=dailycode&age=%d"
.formatted(I + 1)))
.build();
response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
System.out.printf("%d : response = %s%n", I, response);
countDownLatch.countDown();
} catch (IOException e) {
e.printStackTrace(System.err);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace(System.err);
}
});
threadList.add(vThread);
}
threadList.forEach(Thread::start);
countDownLatch.await();
System.out.println("all done");
}
}
결과적으로 하나의 아이피에서 너무 많은 요청을 보내게 되면
아래처럼 http response status code = 429 와
로그로 현재 요청이 막혔음을 알려주는 로그가 보인다.
<?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>3.2.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>me.dailycode</groupId>
<artifactId>hack-test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>hack-test</name>
<description>hack-test</description>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
package me.dailycode.hacktest.simple;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class SimpleHttpRequestJsonBodyTest {
private static final ObjectMapper MAPPER = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
private static final JsonNodeFactory NODE_FACTORY = JsonNodeFactory.instance;
@Test
void simpleHttpRequestTest() {
ObjectNode objectNode = NODE_FACTORY.objectNode();
objectNode.put("name", "dailyCode");
objectNode.put("age", 21);
try (HttpClient httpClient = HttpClient.newHttpClient()) {
HttpRequest httpRequest = HttpRequest.newBuilder(URI.create("http://localhost:8081/simple"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(objectNode.toString()))
.build();
HttpResponse<String> response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
int statusCode = response.statusCode();
String body = response.body();
System.out.println("statusCode = " + statusCode);
JsonNode jsonNode = MAPPER.readValue(body, JsonNode.class);
System.out.println("repsonse body = " + jsonNode.toPrettyString());
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}
}