BaseRestController - IP white 적용
UMS 연동 코드 개선
This commit is contained in:
@@ -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/**" />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user