-
Notifications
You must be signed in to change notification settings - Fork 2
MOSU-191 feat: 분당 50번 이상 요청 시 1분 동안 요청 거부하도록 ratelimit 설정 #192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
db0ef27
5ee29b1
8ad6d96
f8e60ce
300ab07
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package life.mosu.mosuserver.global.config; | ||
|
|
||
| import jakarta.validation.constraints.Min; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import lombok.Data; | ||
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.validation.annotation.Validated; | ||
|
|
||
| @Data | ||
| @Component | ||
| @ConfigurationProperties(prefix = "ratelimit") | ||
| @Validated | ||
| public class IpRateLimitingProperties { | ||
|
|
||
| @Min(1) | ||
| private int maxRequestsPerMinute; | ||
| @Min(1) | ||
| private long timeWindowMs; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| package life.mosu.mosuserver.global.filter; | ||
|
|
||
| import static life.mosu.mosuserver.global.util.IpUtil.getClientIp; | ||
|
|
||
| import com.github.benmanes.caffeine.cache.Cache; | ||
| import com.github.benmanes.caffeine.cache.Caffeine; | ||
| import jakarta.servlet.FilterChain; | ||
| import jakarta.servlet.ServletException; | ||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import java.io.IOException; | ||
| import java.util.concurrent.TimeUnit; | ||
| import life.mosu.mosuserver.global.config.IpRateLimitingProperties; | ||
| import life.mosu.mosuserver.global.exception.CustomRuntimeException; | ||
| import life.mosu.mosuserver.global.exception.ErrorCode; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.web.filter.OncePerRequestFilter; | ||
|
|
||
| @Slf4j | ||
| @Component | ||
| public class IpRateLimitingFilter extends OncePerRequestFilter { | ||
|
|
||
| private final IpRateLimitingProperties ipRateLimitingProperties; | ||
| private final Cache<String, RequestCounter> ipRequestCounts; | ||
|
|
||
| public IpRateLimitingFilter(IpRateLimitingProperties ipRateLimitingProperties) { | ||
| this.ipRateLimitingProperties = ipRateLimitingProperties; | ||
| this.ipRequestCounts = Caffeine.newBuilder() | ||
| .expireAfterWrite(ipRateLimitingProperties.getTimeWindowMs(), TimeUnit.MILLISECONDS) | ||
| .recordStats() | ||
| .build(); | ||
| } | ||
|
|
||
| @Override | ||
| protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) | ||
| throws ServletException, IOException { | ||
|
|
||
| String ip = getClientIp(request); | ||
| RequestCounter counter = ipRequestCounts.get(ip, k -> new RequestCounter()); | ||
|
|
||
| synchronized (counter) { | ||
| counter.increment(); | ||
|
|
||
| if (isBlocked(counter)){ | ||
| log.warn("차단된 IP: {}, 요청 횟수: {}", ip, counter.count); | ||
| handleBlockedIp(); | ||
| } | ||
| } | ||
|
|
||
| log.info("IP: {}, 요청 횟수 증가 후: {}", ip, counter.count); | ||
| log.debug("Cache stats: {}", ipRequestCounts.stats()); | ||
|
|
||
| filterChain.doFilter(request, response); | ||
| } | ||
|
|
||
| private boolean isBlocked(RequestCounter counter) { | ||
| return counter.count >= ipRateLimitingProperties.getMaxRequestsPerMinute(); | ||
| } | ||
|
|
||
| private void handleBlockedIp(){ | ||
| throw new CustomRuntimeException(ErrorCode.TOO_MANY_REQUESTS); | ||
| } | ||
|
|
||
| private static class RequestCounter { | ||
| int count = 0; | ||
|
|
||
| void increment() { | ||
| count++; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package life.mosu.mosuserver.global.util; | ||
|
|
||
| import jakarta.servlet.http.HttpServletRequest; | ||
| import java.net.InetAddress; | ||
| import java.net.UnknownHostException; | ||
|
|
||
| public class IpUtil { | ||
| public static String getClientIp(HttpServletRequest request) throws UnknownHostException { | ||
| String ip = request.getHeader("X-Forwarded-For"); | ||
|
|
||
| if (ip != null && ip.contains(",")) { | ||
| ip = ip.split(",")[0].trim(); | ||
| } | ||
|
|
||
| if (isInvalidIp(ip)) ip = request.getHeader("Proxy-Client-IP"); | ||
| if (isInvalidIp(ip)) ip = request.getHeader("WL-Proxy-Client-IP"); | ||
| if (isInvalidIp(ip)) ip = request.getHeader("HTTP_CLIENT_IP"); | ||
| if (isInvalidIp(ip)) ip = request.getHeader("HTTP_X_FORWARDED_FOR"); | ||
| if (isInvalidIp(ip)) ip = request.getHeader("X-Real-IP"); | ||
| if (isInvalidIp(ip)) ip = request.getRemoteAddr(); | ||
|
|
||
| if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) { | ||
| InetAddress inetAddress = InetAddress.getLocalHost(); | ||
| ip = inetAddress.getHostName() + "/" + inetAddress.getHostAddress(); | ||
| } | ||
|
Comment on lines
+22
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Improve loopback address handling for rate limiting. Several issues with the current approach:
For rate limiting, consider using a consistent identifier for local requests: - if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) {
- InetAddress inetAddress = InetAddress.getLocalHost();
- ip = inetAddress.getHostName() + "/" + inetAddress.getHostAddress();
- }
+ if (isLoopbackAddress(ip)) {
+ ip = "localhost";
+ }Add helper method: private static boolean isLoopbackAddress(String ip) {
return ip != null && (ip.startsWith("127.") ||
"::1".equals(ip) ||
"0:0:0:0:0:0:0:1".equals(ip));
}🤖 Prompt for AI Agents |
||
|
|
||
| return ip; | ||
| } | ||
|
|
||
| private static boolean isInvalidIp(String ip) { | ||
| return ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -48,4 +48,8 @@ pbkdf2: | |||||
|
|
||||||
| login: | ||||||
| max-attempt: 5 | ||||||
| lock-time-milli-seconds: 60000 # 5 minutes | ||||||
| lock-time-milli-seconds: 60000 # 5 minutes | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix incorrect comment The comment says "5 minutes" but the value 60000 milliseconds is actually 1 minute. - lock-time-milli-seconds: 60000 # 5 minutes
+ lock-time-milli-seconds: 60000 # 1 minute📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
|
|
||||||
| ratelimit: | ||||||
| max-requests-per-minute: 50 | ||||||
| time-window-ms: 60000 # 1 minute | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Consider exception handling strategy for rate limiting.
The method throws
UnknownHostExceptionwhich should be handled gracefully in the rate limiting filter to avoid service disruption.Verify how exceptions are handled in the calling
IpRateLimitingFilter:🏁 Script executed:
Length of output: 91
Let’s locate and inspect how
getClientIpis invoked and handled in the filter:🏁 Script executed:
Length of output: 2356
Let’s inspect the
doFilterInternalbody to see ifUnknownHostExceptionis caught or just propagated:🏁 Script executed:
Length of output: 3188
Handle
UnknownHostExceptioninIpRateLimitingFilterThe call to
getClientIp(request)(which throwsUnknownHostException) isn’t caught, so any DNS lookup failure will bubble up as anIOExceptionand abort the filter. To avoid service disruption and ensure rate limiting still proceeds, wrap this call in atry–catchand fall back to a safe default IP (or log and treat it as “unknown”).Locations to update:
src/main/java/life/mosu/mosuserver/global/filter/IpRateLimitingFilter.javadoFilterInternal)Suggested diff:
📝 Committable suggestion
🤖 Prompt for AI Agents