BaseRestController - IP white 적용

UMS 연동 코드 개선
This commit is contained in:
Rinjae
2026-02-02 15:32:23 +09:00
parent cd3e0f1dba
commit 878f1da4ad
6 changed files with 312 additions and 3 deletions
+10
View File
@@ -112,6 +112,16 @@
<mvc:mapping path="/_onl/**"/> <mvc:mapping path="/_onl/**"/>
<bean class="com.eactive.eai.rms.common.interceptor.ApiKeyInterceptor" /> <bean class="com.eactive.eai.rms.common.interceptor.ApiKeyInterceptor" />
</mvc:interceptor> </mvc:interceptor>
<!-- BaseRestController 엔드포인트 IP 화이트리스트 인증 -->
<mvc:interceptor>
<mvc:mapping path="/api/**"/>
<mvc:mapping path="/kjb/**"/>
<mvc:mapping path="/control/**"/>
<mvc:mapping path="/metrics/**"/>
<mvc:mapping path="/apis/**"/>
<mvc:mapping path="/transaction/**"/>
<bean class="com.eactive.eai.rms.common.interceptor.IpWhitelistInterceptor" />
</mvc:interceptor>
<mvc:interceptor> <mvc:interceptor>
<mvc:mapping path="/**" /> <mvc:mapping path="/**" />
<mvc:exclude-mapping path="/kjb/**" /> <mvc:exclude-mapping path="/kjb/**" />
+1 -1
View File
@@ -9,7 +9,7 @@
<timeout-secs>3600</timeout-secs> <timeout-secs>3600</timeout-secs>
<cookie-name>JSESSIONID_EMS</cookie-name> <cookie-name>JSESSIONID_EMS</cookie-name>
<persistent-store-type>memory</persistent-store-type> <persistent-store-type>memory</persistent-store-type>
<!-- <persistent-store-type>replicated_if_clustered</persistent-store-type> --> <persistent-store-type>replicated_if_clustered</persistent-store-type>
</session-descriptor> </session-descriptor>
<container-descriptor> <container-descriptor>
@@ -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 화이트리스트 인증 인터셉터
*
* <p>BaseRestController를 상속받은 컨트롤러에 대해 IP 인증을 수행합니다.</p>
*
* <p>동작 흐름:</p>
* <ol>
* <li>핸들러가 BaseRestController 인스턴스인지 확인</li>
* <li>클라이언트 IP 추출 (프록시 헤더 지원)</li>
* <li>IP 화이트리스트 대비 검증</li>
* <li>검증 실패 시 HTTP 403 Forbidden 반환</li>
* </ol>
*
* <p>보안:</p>
* <ul>
* <li>차단된 요청은 모두 로그에 기록됩니다</li>
* <li>IP 화이트리스트는 kjb.api.allow_ip_list 속성에서 관리됩니다</li>
* </ul>
*
* @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));
}
}
@@ -6,7 +6,7 @@ import com.google.gson.GsonBuilder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
/** /**
* REST API Controller 기본 클래스 * 외부 API 엔드포인트를 위한 Base REST Controller
* *
* <p>기능:</p> * <p>기능:</p>
* <ul> * <ul>
@@ -15,6 +15,13 @@ import java.nio.charset.StandardCharsets;
* <li>응답 크기 포맷팅</li> * <li>응답 크기 포맷팅</li>
* </ul> * </ul>
* *
* <p>보안:</p>
* <ul>
* <li>세션 인증 생략 (InterceptorSkipController 구현)</li>
* <li>IP 화이트리스트 인증 필요 (IpWhitelistInterceptor를 통해)</li>
* <li>허용된 IP는 속성에서 설정: kjb.api.allow_ip_list</li>
* </ul>
*
* <p>사용 예:</p> * <p>사용 예:</p>
* <pre> * <pre>
* {@code * {@code
@@ -30,7 +37,7 @@ import java.nio.charset.StandardCharsets;
* } * }
* </pre> * </pre>
* *
* TODO: API Key 또는 IP 화이트리스트 인증 기능 추가 고려 * @see com.eactive.eai.rms.common.interceptor.IpWhitelistInterceptor
*/ */
public abstract class BaseRestController implements InterceptorSkipController { public abstract class BaseRestController implements InterceptorSkipController {
@@ -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 화이트리스트 인증 서비스
*
* <p>기능:</p>
* <ul>
* <li>IP 화이트리스트 관리 및 검증</li>
* <li>캐싱을 통한 성능 최적화 (60초 TTL)</li>
* <li>와일드카드(*), CIDR, 정확한 매칭 지원</li>
* </ul>
*
* <p>캐싱 전략:</p>
* <ul>
* <li>캐시 TTL: 60초</li>
* <li>스레드 안전성: volatile + synchronized 이중 체크 락킹</li>
* </ul>
*
* @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");
}
}
}
@@ -1,5 +1,7 @@
package com.eactive.ext.kjb.util; 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.eai.rms.data.entity.man.monitoringProperty.service.MonitoringPropertyService;
import com.eactive.ext.kjb.common.KjbProperty; import com.eactive.ext.kjb.common.KjbProperty;
import com.eactive.ext.kjb.common.KjbPropertyValue; import com.eactive.ext.kjb.common.KjbPropertyValue;
@@ -7,6 +9,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.Optional;
@Slf4j @Slf4j
public class KjbPropertyInjector { public class KjbPropertyInjector {
@@ -69,6 +72,11 @@ public class KjbPropertyInjector {
if (anno != null) { if (anno != null) {
String key = anno.key(); String key = anno.key();
// DB에 속성이 없으면 기본값으로 자동 생성
ensurePropertyExists(groupId, key, anno.defaultValue());
// 값 조회
String rawValue = monitoringPropertyService.getPropertyValue(groupId, key, anno.defaultValue()); String rawValue = monitoringPropertyService.getPropertyValue(groupId, key, anno.defaultValue());
Object convertedValue = convertValue(field.getType(), rawValue); Object convertedValue = convertValue(field.getType(), rawValue);
@@ -85,6 +93,32 @@ public class KjbPropertyInjector {
return property; 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<MonitoringProperty> 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) { private static Object convertValue(Class<?> type, String rawValue) {
if (rawValue == null) return null; if (rawValue == null) return null;