diff --git a/WebContent/WEB-INF/springapp-servlet.xml b/WebContent/WEB-INF/springapp-servlet.xml index 89c338a..66d4464 100644 --- a/WebContent/WEB-INF/springapp-servlet.xml +++ b/WebContent/WEB-INF/springapp-servlet.xml @@ -112,6 +112,16 @@ + + + + + + + + + + diff --git a/WebContent/WEB-INF/weblogic.xml b/WebContent/WEB-INF/weblogic.xml index 14d1451..ec07529 100644 --- a/WebContent/WEB-INF/weblogic.xml +++ b/WebContent/WEB-INF/weblogic.xml @@ -9,7 +9,7 @@ 3600 JSESSIONID_EMS memory - + replicated_if_clustered 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 인증을 수행합니다.

+ * + *

동작 흐름:

+ *
    + *
  1. 핸들러가 BaseRestController 인스턴스인지 확인
  2. + *
  3. 클라이언트 IP 추출 (프록시 헤더 지원)
  4. + *
  5. IP 화이트리스트 대비 검증
  6. + *
  7. 검증 실패 시 HTTP 403 Forbidden 반환
  8. + *
+ * + *

보안:

+ *
    + *
  • 차단된 요청은 모두 로그에 기록됩니다
  • + *
  • 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;