diff --git a/src/main/java/com/eactive/eai/rms/common/interceptor/IpWhitelistInterceptor.java b/src/main/java/com/eactive/eai/rms/common/interceptor/IpWhitelistInterceptor.java
new file mode 100644
index 0000000..b4a99de
--- /dev/null
+++ b/src/main/java/com/eactive/eai/rms/common/interceptor/IpWhitelistInterceptor.java
@@ -0,0 +1,96 @@
+package com.eactive.eai.rms.common.interceptor;
+
+import com.eactive.eai.rms.common.util.IpUtil;
+import com.eactive.eai.rms.onl.apim.common.BaseRestController;
+import com.eactive.eai.rms.onl.apim.service.IpAuthenticationService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.web.method.HandlerMethod;
+import org.springframework.web.servlet.HandlerInterceptor;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * IP 화이트리스트 인증 인터셉터
+ *
+ * BaseRestController를 상속받은 컨트롤러에 대해 IP 인증을 수행합니다.
+ *
+ * 동작 흐름:
+ *
+ * - 핸들러가 BaseRestController 인스턴스인지 확인
+ * - 클라이언트 IP 추출 (프록시 헤더 지원)
+ * - IP 화이트리스트 대비 검증
+ * - 검증 실패 시 HTTP 403 Forbidden 반환
+ *
+ *
+ * 보안:
+ *
+ * - 차단된 요청은 모두 로그에 기록됩니다
+ * - IP 화이트리스트는 kjb.api.allow_ip_list 속성에서 관리됩니다
+ *
+ *
+ * @see BaseRestController
+ * @see IpAuthenticationService
+ */
+@Component
+@Slf4j
+public class IpWhitelistInterceptor implements HandlerInterceptor {
+
+ @Autowired
+ private IpAuthenticationService ipAuthenticationService;
+
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+ // BaseRestController를 상속받은 컨트롤러만 IP 인증 적용
+ if (!isBaseRestController(handler)) {
+ return true;
+ }
+
+ String clientIp = IpUtil.getClientIp(request);
+ String requestUri = request.getRequestURI();
+
+ if (ipAuthenticationService.isIpAllowed(clientIp)) {
+ if (log.isDebugEnabled()) {
+ log.debug("IP authentication success - IP: {}, URI: {}", clientIp, requestUri);
+ }
+ return true;
+ }
+
+ // IP 인증 실패
+ log.warn("IP authentication failed - IP: {}, URI: {}", clientIp, requestUri);
+ sendErrorResponse(response, HttpServletResponse.SC_FORBIDDEN, "Access denied: IP address not in whitelist");
+ return false;
+ }
+
+ /**
+ * 핸들러가 BaseRestController 인스턴스인지 확인
+ *
+ * @param handler 핸들러 객체
+ * @return BaseRestController 인스턴스이면 true
+ */
+ private boolean isBaseRestController(Object handler) {
+ if (handler instanceof HandlerMethod) {
+ HandlerMethod handlerMethod = (HandlerMethod) handler;
+ Object controller = handlerMethod.getBean();
+ return controller instanceof BaseRestController;
+ }
+ return false;
+ }
+
+ /**
+ * 에러 응답 전송
+ *
+ * @param response HttpServletResponse
+ * @param status HTTP 상태 코드
+ * @param message 에러 메시지
+ * @throws IOException I/O 오류 발생 시
+ */
+ private void sendErrorResponse(HttpServletResponse response, int status, String message) throws IOException {
+ response.setStatus(status);
+ response.setContentType("application/json;charset=UTF-8");
+ response.getWriter().write(String.format("{\"error\": \"%s\"}", message));
+ }
+}
diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/common/BaseRestController.java b/src/main/java/com/eactive/eai/rms/onl/apim/common/BaseRestController.java
index 057a983..16eb851 100644
--- a/src/main/java/com/eactive/eai/rms/onl/apim/common/BaseRestController.java
+++ b/src/main/java/com/eactive/eai/rms/onl/apim/common/BaseRestController.java
@@ -6,7 +6,7 @@ import com.google.gson.GsonBuilder;
import java.nio.charset.StandardCharsets;
/**
- * REST API Controller 기본 클래스
+ * 외부 API 엔드포인트를 위한 Base REST Controller
*
* 기능:
*
@@ -15,6 +15,13 @@ import java.nio.charset.StandardCharsets;
* - 응답 크기 포맷팅
*
*
+ * 보안:
+ *
+ * - 세션 인증 생략 (InterceptorSkipController 구현)
+ * - IP 화이트리스트 인증 필요 (IpWhitelistInterceptor를 통해)
+ * - 허용된 IP는 속성에서 설정: kjb.api.allow_ip_list
+ *
+ *
* 사용 예:
*
* {@code
@@ -30,7 +37,7 @@ import java.nio.charset.StandardCharsets;
* }
*
*
- * TODO: API Key 또는 IP 화이트리스트 인증 기능 추가 고려
+ * @see com.eactive.eai.rms.common.interceptor.IpWhitelistInterceptor
*/
public abstract class BaseRestController implements InterceptorSkipController {
diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/service/IpAuthenticationService.java b/src/main/java/com/eactive/eai/rms/onl/apim/service/IpAuthenticationService.java
new file mode 100644
index 0000000..753c7eb
--- /dev/null
+++ b/src/main/java/com/eactive/eai/rms/onl/apim/service/IpAuthenticationService.java
@@ -0,0 +1,162 @@
+package com.eactive.eai.rms.onl.apim.service;
+
+import com.eactive.eai.util.IpUtil;
+import com.eactive.ext.kjb.common.KjbPropertyHolder;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+/**
+ * IP 화이트리스트 인증 서비스
+ *
+ * 기능:
+ *
+ * - IP 화이트리스트 관리 및 검증
+ * - 캐싱을 통한 성능 최적화 (60초 TTL)
+ * - 와일드카드(*), CIDR, 정확한 매칭 지원
+ *
+ *
+ * 캐싱 전략:
+ *
+ * - 캐시 TTL: 60초
+ * - 스레드 안전성: volatile + synchronized 이중 체크 락킹
+ *
+ *
+ * @see com.eactive.eai.util.IpUtil#isMatchIp(String, String)
+ */
+@Service
+@Slf4j
+public class IpAuthenticationService {
+
+ /**
+ * 캐시 TTL (밀리초)
+ */
+ private static final long CACHE_TTL_MS = 60_000L; // 60초
+
+ /**
+ * 캐시된 IP 화이트리스트
+ */
+ private volatile String cachedWhitelist = null;
+
+ /**
+ * 캐시 만료 시간 (밀리초)
+ */
+ private volatile long cacheExpirationTime = 0L;
+
+ /**
+ * 클라이언트 IP가 화이트리스트에 포함되는지 확인
+ *
+ * @param clientIp 클라이언트 IP 주소
+ * @return 화이트리스트에 포함되면 true, 아니면 false
+ */
+ public boolean isIpAllowed(String clientIp) {
+ if (StringUtils.isBlank(clientIp)) {
+ log.warn("Client IP is blank, denying access");
+ return false;
+ }
+
+ String whitelist = getWhitelistWithCache();
+
+ if (StringUtils.isBlank(whitelist)) {
+ log.warn("IP whitelist is empty, denying all access");
+ return false;
+ }
+
+ try {
+ boolean allowed = IpUtil.isMatchIp(whitelist, clientIp);
+ if (log.isDebugEnabled()) {
+ log.debug("IP authentication - IP: {}, Allowed: {}, Whitelist: {}", clientIp, allowed, whitelist);
+ }
+ return allowed;
+ } catch (Exception e) {
+ log.error("Error checking IP whitelist - IP: {}, Whitelist: {}", clientIp, whitelist, e);
+ return false;
+ }
+ }
+
+ /**
+ * 캐시된 화이트리스트 반환, 만료 시 재로딩
+ *
+ * @return IP 화이트리스트 (쉼표로 구분된 IP 패턴)
+ */
+ private String getWhitelistWithCache() {
+ long currentTime = System.currentTimeMillis();
+
+ // 캐시가 유효한 경우 캐시 반환
+ if (cachedWhitelist != null && currentTime < cacheExpirationTime) {
+ return cachedWhitelist;
+ }
+
+ // 캐시 갱신 (이중 체크 락킹)
+ synchronized (this) {
+ // 다른 스레드가 이미 갱신했는지 재확인
+ if (cachedWhitelist != null && currentTime < cacheExpirationTime) {
+ return cachedWhitelist;
+ }
+
+ // DB에서 화이트리스트 로드
+ String whitelist = loadWhitelistFromDb();
+
+ // 공백 제거 및 캐시 저장
+ cachedWhitelist = trimWhitelist(whitelist);
+ cacheExpirationTime = currentTime + CACHE_TTL_MS;
+
+ log.info("IP whitelist cache refreshed - Whitelist: {}", cachedWhitelist);
+ return cachedWhitelist;
+ }
+ }
+
+ /**
+ * DB에서 IP 화이트리스트 로드
+ *
+ * @return IP 화이트리스트 문자열
+ */
+ private String loadWhitelistFromDb() {
+ try {
+ String whitelist = KjbPropertyHolder.get().getApiAllowIpList();
+ if (StringUtils.isBlank(whitelist)) {
+ log.warn("IP whitelist property is empty");
+ return "";
+ }
+ return whitelist;
+ } catch (Exception e) {
+ log.error("Failed to load IP whitelist from database", e);
+ return "";
+ }
+ }
+
+ /**
+ * 화이트리스트의 각 IP 패턴에서 공백 제거
+ *
+ * @param whitelist 원본 화이트리스트 (쉼표로 구분)
+ * @return 공백이 제거된 화이트리스트
+ */
+ private String trimWhitelist(String whitelist) {
+ if (StringUtils.isBlank(whitelist)) {
+ return "";
+ }
+
+ String[] patterns = whitelist.split(",");
+ StringBuilder trimmed = new StringBuilder();
+
+ for (int i = 0; i < patterns.length; i++) {
+ if (i > 0) {
+ trimmed.append(",");
+ }
+ trimmed.append(patterns[i].trim());
+ }
+
+ return trimmed.toString();
+ }
+
+ /**
+ * 캐시 강제 갱신 (테스트용)
+ */
+ public void clearCache() {
+ synchronized (this) {
+ cachedWhitelist = null;
+ cacheExpirationTime = 0L;
+ log.info("IP whitelist cache cleared");
+ }
+ }
+}
diff --git a/src/main/java/com/eactive/ext/kjb/util/KjbPropertyInjector.java b/src/main/java/com/eactive/ext/kjb/util/KjbPropertyInjector.java
index 414bd4d..8d2dfb5 100644
--- a/src/main/java/com/eactive/ext/kjb/util/KjbPropertyInjector.java
+++ b/src/main/java/com/eactive/ext/kjb/util/KjbPropertyInjector.java
@@ -1,5 +1,7 @@
package com.eactive.ext.kjb.util;
+import com.eactive.eai.rms.data.entity.man.monitoringProperty.MonitoringProperty;
+import com.eactive.eai.rms.data.entity.man.monitoringProperty.MonitoringPropertyId;
import com.eactive.eai.rms.data.entity.man.monitoringProperty.service.MonitoringPropertyService;
import com.eactive.ext.kjb.common.KjbProperty;
import com.eactive.ext.kjb.common.KjbPropertyValue;
@@ -7,6 +9,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.lang.reflect.Field;
+import java.util.Optional;
@Slf4j
public class KjbPropertyInjector {
@@ -69,6 +72,11 @@ public class KjbPropertyInjector {
if (anno != null) {
String key = anno.key();
+
+ // DB에 속성이 없으면 기본값으로 자동 생성
+ ensurePropertyExists(groupId, key, anno.defaultValue());
+
+ // 값 조회
String rawValue = monitoringPropertyService.getPropertyValue(groupId, key, anno.defaultValue());
Object convertedValue = convertValue(field.getType(), rawValue);
@@ -85,6 +93,32 @@ public class KjbPropertyInjector {
return property;
}
+ /**
+ * 속성이 DB에 존재하는지 확인하고, 없으면 기본값으로 생성
+ *
+ * @param groupName 속성 그룹명
+ * @param key 속성 키
+ * @param defaultValue 기본값
+ */
+ private void ensurePropertyExists(String groupName, String key, String defaultValue) {
+ MonitoringPropertyId id = new MonitoringPropertyId();
+ id.setPrptyGroupName(groupName);
+ id.setPrptyName(key);
+
+ Optional existing = monitoringPropertyService.findById(id);
+
+ if (!existing.isPresent()) {
+ log.info("Property not found in DB, creating - Group: {}, Key: {}, DefaultValue: {}", groupName, key, defaultValue);
+
+ MonitoringProperty property = new MonitoringProperty();
+ property.setId(id);
+ property.setPrpty2Val(defaultValue);
+
+ monitoringPropertyService.save(property);
+ log.info("Property created successfully - Group: {}, Key: {}", groupName, key);
+ }
+ }
+
private static Object convertValue(Class> type, String rawValue) {
if (rawValue == null) return null;