Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ dependencies {
// security
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'

// aws
implementation platform('software.amazon.awssdk:bom:2.20.0')
Expand Down
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
Expand Up @@ -11,6 +11,7 @@ public enum ErrorCode {
// Principal 관련 에러
PRINCIPAL_NOT_FOUND(HttpStatus.UNAUTHORIZED, "인증된 사용자를 찾을 수 없습니다."),
LOGIN_BLOCKED(HttpStatus.UNAUTHORIZED, "로그인 시도가 차단되었습니다. 잠시 후 다시 시도해주세요."),
TOO_MANY_REQUESTS(HttpStatus.TOO_MANY_REQUESTS, "요청이 너무 많습니다. 잠시 후 다시 시도해주세요."),

// OAuth 관련 에러
UNSUPPORTED_OAUTH2_PROVIDER(HttpStatus.BAD_REQUEST, "지원하지 않는 OAuth2 제공자입니다."),
Expand Down
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++;
}
}
}
33 changes: 33 additions & 0 deletions src/main/java/life/mosu/mosuserver/global/util/IpUtil.java
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 {
Copy link

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 UnknownHostException which should be handled gracefully in the rate limiting filter to avoid service disruption.

Verify how exceptions are handled in the calling IpRateLimitingFilter:


🏁 Script executed:

#!/bin/bash
# Description: Check exception handling in the rate limiting filter
# Expected: Proper exception handling to avoid service disruption

ast-grep --pattern 'class IpRateLimitingFilter {
  $$$
  IpUtil.getClientIp($$$)
  $$$
}'

Length of output: 91


Let’s locate and inspect how getClientIp is invoked and handled in the filter:


🏁 Script executed:

#!/bin/bash
# 1. Find the filter’s source file
fd IpRateLimitingFilter.java

# 2. Show its class declaration and import section for context
rg -n "class IpRateLimitingFilter" -C5

# 3. Find and display calls to getClientIp within that class
rg -n "IpUtil.getClientIp" -C5

Length of output: 2356


Let’s inspect the doFilterInternal body to see if UnknownHostException is caught or just propagated:


🏁 Script executed:

#!/bin/bash
# 1. Show the doFilterInternal signature and surrounding lines
rg -n "protected void doFilterInternal" -C20 src/main/java/life/mosu/mosuserver/global/filter/IpRateLimitingFilter.java

# 2. Show calls to getClientIp within that method
rg -n "getClientIp" -C10 src/main/java/life/mosu/mosuserver/global/filter/IpRateLimitingFilter.java

Length of output: 3188


Handle UnknownHostException in IpRateLimitingFilter

The call to getClientIp(request) (which throws UnknownHostException) isn’t caught, so any DNS lookup failure will bubble up as an IOException and abort the filter. To avoid service disruption and ensure rate limiting still proceeds, wrap this call in a try–catch and fall back to a safe default IP (or log and treat it as “unknown”).

Locations to update:

  • File: src/main/java/life/mosu/mosuserver/global/filter/IpRateLimitingFilter.java
  • Around line 39 (inside doFilterInternal)

Suggested diff:

--- a/src/main/java/life/mosu/mosuserver/global/filter/IpRateLimitingFilter.java
+++ b/src/main/java/life/mosu/mosuserver/global/filter/IpRateLimitingFilter.java
@@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
-        String ip = getClientIp(request);
+        String ip;
+        try {
+            ip = getClientIp(request);
+        } catch (UnknownHostException e) {
+            log.warn("Failed to resolve client IP, defaulting to 0.0.0.0", e);
+            ip = "0.0.0.0";
+        }
         RequestCounter counter = ipRequestCounts.get(ip, k -> new RequestCounter());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static String getClientIp(HttpServletRequest request) throws UnknownHostException {
--- a/src/main/java/life/mosu/mosuserver/global/filter/IpRateLimitingFilter.java
+++ b/src/main/java/life/mosu/mosuserver/global/filter/IpRateLimitingFilter.java
@@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
- String ip = getClientIp(request);
+ String ip;
+ try {
+ ip = getClientIp(request);
+ } catch (UnknownHostException e) {
+ log.warn("Failed to resolve client IP, defaulting to 0.0.0.0", e);
+ ip = "0.0.0.0";
+ }
RequestCounter counter = ipRequestCounts.get(ip, k -> new RequestCounter());
// ... rest of filter logic
}
🤖 Prompt for AI Agents
In src/main/java/life/mosu/mosuserver/global/filter/IpRateLimitingFilter.java
around line 39, the call to getClientIp(request) throws UnknownHostException
which is not currently caught, causing the filter to abort on DNS failures. Wrap
the getClientIp(request) call in a try-catch block that catches
UnknownHostException, and in the catch block, log the exception and assign a
safe default IP value or a placeholder like "unknown" to ensure the rate
limiting logic continues without interruption.

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
Copy link

Choose a reason for hiding this comment

The 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:

  1. Incomplete loopback detection: Only checks specific addresses, missing other loopback ranges (127.x.x.x, ::1)
  2. Unusual format: The "hostname/hostaddress" format may not work well with rate limiting logic
  3. Unreliable fallback: InetAddress.getLocalHost() can be unreliable in containerized environments

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
In src/main/java/life/mosu/mosuserver/global/util/IpUtil.java around lines 22 to
25, improve loopback address handling by replacing the current exact string
checks with a helper method that detects all loopback addresses, including the
full 127.x.x.x range and both IPv6 loopback forms (::1 and 0:0:0:0:0:0:0:1).
Remove the use of InetAddress.getLocalHost() and avoid combining hostname and IP
in a single string. Instead, use a consistent identifier for local requests,
such as a fixed string like "LOCAL_LOOPBACK", to ensure reliable and uniform
rate limiting behavior.


return ip;
}

private static boolean isInvalidIp(String ip) {
return ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip);
}
}
6 changes: 5 additions & 1 deletion src/main/resources/security-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,8 @@ pbkdf2:

login:
max-attempt: 5
lock-time-milli-seconds: 60000 # 5 minutes
lock-time-milli-seconds: 60000 # 5 minutes
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
lock-time-milli-seconds: 60000 # 5 minutes
lock-time-milli-seconds: 60000 # 1 minute
🤖 Prompt for AI Agents
In src/main/resources/security-config.yml at line 51, the comment incorrectly
states "5 minutes" while the value 60000 milliseconds corresponds to 1 minute.
Update the comment to accurately reflect the value by changing "5 minutes" to "1
minute" or adjust the value to 300000 if the intended lock time is 5 minutes.


ratelimit:
max-requests-per-minute: 50
time-window-ms: 60000 # 1 minute