diff --git a/src/main/java/com/eactive/apim/portal/djb/menu/MenuInternalController.java b/src/main/java/com/eactive/apim/portal/djb/menu/MenuInternalController.java index 1663d58..75ea49c 100644 --- a/src/main/java/com/eactive/apim/portal/djb/menu/MenuInternalController.java +++ b/src/main/java/com/eactive/apim/portal/djb/menu/MenuInternalController.java @@ -1,5 +1,6 @@ package com.eactive.apim.portal.djb.menu; +import com.eactive.apim.portal.common.util.IpAddressMatcher; import com.eactive.apim.portal.portalproperty.service.PortalPropertyService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -12,11 +13,8 @@ import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; -import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; /** * 메뉴 캐시 내부 API — eapim-admin 의 reload 명령 수신용. @@ -24,6 +22,8 @@ import java.util.stream.Collectors; *

가드: PTL_PROPERTY {@code Portal / menu.internal.allow-ips} 허용 IP 목록 * (기본 loopback) + X-Forwarded-For 동반 요청 거부 * ({@code LegacyEncryptionMigrationController.assertLocalOnly} 모델). + * 허용 목록은 {@link IpAddressMatcher} 규칙을 따라 정확 일치 외에 + * IPv4 CIDR({@code 172.30.1.0/24}) 과 옥텟 와일드카드({@code 172.30.*.*}) 를 지원한다. * CSRF 는 PortalConfigSecurity 에서 {@code /internal/menu/**} 예외 처리.

* *
curl -X POST http://127.0.0.1:39130/internal/menu/reload
@@ -37,6 +37,9 @@ public class MenuInternalController { static final String PROP_GROUP = "Portal"; static final String PROP_ALLOW_IPS = "menu.internal.allow-ips"; static final String DEFAULT_ALLOW_IPS = "127.0.0.1,::1"; + static final String PROP_ALLOW_IPS_DESCRIPTION = + "메뉴 내부 API(리로드) 허용 IP 목록. 콤마(,)/세미콜론(;)/줄바꿈 구분, " + + "정확일치·IPv4 CIDR(172.30.1.0/24)·와일드카드(172.30.*.*) 지원"; private final MenuService menuService; private final PortalPropertyService portalPropertyService; @@ -66,16 +69,10 @@ public class MenuInternalController { * 그 경우에도 allowlist 검사로 차단된다. WebLogic WAR 배포에서는 원 소켓 IP 로 검사된다.) */ private boolean isAllowed(HttpServletRequest request) { - String remote = canonicalize(request.getRemoteAddr()); + String remote = IpAddressMatcher.canonicalize(request.getRemoteAddr()); boolean viaProxy = request.getHeader("X-Forwarded-For") != null; - Set allowed = Arrays.stream(resolveAllowIps().split(",")) - .map(String::trim) - .filter(ip -> !ip.isEmpty()) - .map(MenuInternalController::canonicalize) - .collect(Collectors.toSet()); - - if (viaProxy || !allowed.contains(remote)) { + if (viaProxy || !IpAddressMatcher.matches(resolveAllowIps(), remote)) { log.warn("메뉴 내부 API 차단 - remote: {}, viaProxy: {}", remote, viaProxy); return false; } @@ -85,15 +82,10 @@ public class MenuInternalController { private String resolveAllowIps() { try { return portalPropertyService.getOrCreateProperty(PROP_GROUP, PROP_ALLOW_IPS, - DEFAULT_ALLOW_IPS, "메뉴 내부 API(리로드) 허용 IP 목록(콤마 구분)"); + DEFAULT_ALLOW_IPS, PROP_ALLOW_IPS_DESCRIPTION); } catch (Exception e) { log.warn("허용 IP 목록 조회 실패 - 기본값({}) 사용", DEFAULT_ALLOW_IPS, e); return DEFAULT_ALLOW_IPS; } } - - /** IPv6 loopback 표기 통일 */ - private static String canonicalize(String ip) { - return "0:0:0:0:0:0:0:1".equals(ip) ? "::1" : ip; - } }