DJB 커스텀 소스 납품 스크립트 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled

- Windows, Linux/macOS 지원 배치/셸 스크립트 구현
- 소스 필터 규칙 및 수집 파일/모듈 매니페스트 작성
This commit is contained in:
Rinjae
2026-08-11 11:47:31 +09:00
parent 766341540d
commit 0ad5bffa4f
10 changed files with 1337 additions and 0 deletions
@@ -0,0 +1,91 @@
package com.eactive.apim.portal.config;
import javax.servlet.DispatcherType;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.filter.ForwardedHeaderFilter;
/**
* 리버스프록시(OHS) 전달 헤더 처리 필터 등록.
*
* <p>{@code server.forward-headers-strategy=framework} 일 때만 활성화된다. Spring Boot 오토컨피그
* ({@code ServletWebServerFactoryAutoConfiguration.ForwardedHeaderFilterConfiguration}) 가 동일 조건에서
* 기본 {@link ForwardedHeaderFilter} 를 등록하지만, 여기서 직접 등록해 <b>{@code /internal/**} 를 필터 대상에서
* 제외</b>한다. 오토컨피그는 해당 필터 빈이 이미 있으면 물러난다.
*
* <h3>왜 native 가 아니라 framework 인가</h3>
* {@code native} 는 내장 Tomcat 의 {@code RemoteIpValve} 에 위임하는 전략이라 WAR 로 WebLogic 에 배포하면
* 아무 동작도 하지 않는다({@link com.eactive.apim.portal.PortalApplication} 은 {@code SpringBootServletInitializer}
* 상속). {@code framework} 는 평범한 서블릿 필터라 배포 형태와 무관하게 동작한다.
*
* <h3>왜 {@code /internal/**} 를 제외하는가</h3>
* {@link ForwardedHeaderFilter} 는 (1) {@code getRemoteAddr()} 을 {@code X-Forwarded-For} 값으로 치환하고
* (2) 처리한 forwarded 헤더를 요청에서 제거한다. 내부 API 는 이 두 값으로 접근을 통제하므로 그대로 두면 가드가 뒤집힌다.
*
* <pre>
* 공격자: X-Forwarded-For: 127.0.0.1
* → getRemoteAddr() = "127.0.0.1" (위조값이 허용 IP 검사를 통과)
* → getHeader("X-Forwarded-For") = null (필터가 제거 → viaProxy 판정이 false)
* </pre>
*
* 즉 {@code MenuInternalController#isAllowed} / {@code LegacyEncryptionMigrationController#assertAllowedIp}
* 의 "프록시 경유 거부 + loopback 허용" 가드가 무력화된다. 필터를 제외해 두면 이 경로만은 <b>원 소켓 IP</b> 로
* 검사되므로 기존 가드가 설계대로 동작한다.
*
* <p><b>전제</b>: {@code framework} 는 신뢰 프록시 목록이 없어 헤더를 무조건 신뢰한다. 앞단(OHS)에서 인바운드
* {@code X-Forwarded-*} / {@code Forwarded} 를 제거한 뒤 재설정해야 하며, WAS 포트로의 직접 접근 경로도 차단해야 한다.
*
* @see com.eactive.apim.portal.djb.menu.MenuInternalController
* @see com.eactive.apim.portal.common.migration.LegacyEncryptionMigrationController
*/
@Slf4j
@Configuration
@ConditionalOnProperty(name = "server.forward-headers-strategy", havingValue = "framework")
public class PortalConfigForwardedHeader {
/** 프록시 헤더를 신뢰하지 않고 원 소켓 IP 로 판정해야 하는 경로. */
private static final String INTERNAL_PREFIX = "/internal/";
@Bean
public FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilterRegistration() {
FilterRegistrationBean<ForwardedHeaderFilter> registration =
new FilterRegistrationBean<>(new InternalExcludingForwardedHeaderFilter());
// 다른 필터(XSS, Security 등)가 요청을 읽기 전에 scheme/host/port/remoteAddr 이 교정되어야 한다.
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC, DispatcherType.ERROR);
log.info("ForwardedHeaderFilter 등록 - X-Forwarded-* 반영, 제외 경로: {}**", INTERNAL_PREFIX);
return registration;
}
/** {@code /internal/**} 요청은 원본 요청 그대로 통과시키는 {@link ForwardedHeaderFilter}. */
static class InternalExcludingForwardedHeaderFilter extends ForwardedHeaderFilter {
/**
* {@code super} 구현은 "forwarded 계열 헤더가 하나도 없으면 필터링하지 않는다"이다.
* 그 최적화를 유지해야 프록시를 거치지 않는 환경(로컬·내부 직접 접속)의 동작이 기존과 동일하게 남으므로
* 내부 경로 판정과 반드시 OR 로 결합한다.
*/
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return isInternal(request) || super.shouldNotFilter(request);
}
private boolean isInternal(HttpServletRequest request) {
String uri = request.getRequestURI();
if (uri == null) {
return false;
}
// context-path 는 현재 "/" 이지만, 변경되더라도 경로 판정이 어긋나지 않게 접두를 걷어낸다.
String contextPath = request.getContextPath();
if (contextPath != null && contextPath.length() > 1 && uri.startsWith(contextPath)) {
uri = uri.substring(contextPath.length());
}
return uri.startsWith(INTERNAL_PREFIX);
}
}
}
@@ -0,0 +1,45 @@
package com.eactive.apim.portal.djb.guide;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
/**
* 개발가이드 안내페이지에 노출할 값을 {@code PTL_PROPERTY}(그룹 {@code Portal}, key prefix {@code djb.guide.})
* 에서 읽는 얇은 접근자.
*
* <p>안내페이지에 표기되는 게이트웨이 주소는 서비스 환경(dev/stage/prod)마다 달라지므로 템플릿에 하드코딩하지 않고
* DB 프로퍼티로 분리한다. 값 미존재 시 {@link PortalPropertyService#getOrCreateProperty} 가 기본값으로 INSERT 하며,
* 공백 값은 {@link #resolve} 에서 기본값으로 폴백한다({@code getOrCreateProperty} 는 공백 보정을 하지 않음).
*/
@Component
@RequiredArgsConstructor
public class GuideProperty {
private final PortalPropertyService portalPropertyService;
private static final String GROUP = "Portal";
public static final String KEY_GW_URL = "djb.guide.gw-url";
public static final String DEFAULT_GW_URL = "https://openapi.jejubank.co.kr";
/** 안내페이지(OAuth2 개발가이드 등)에 표기할 게이트웨이 base URL. 끝 슬래시는 제거한다. */
public String gatewayUrl() {
return stripTrailingSlash(resolve(KEY_GW_URL, DEFAULT_GW_URL,
"개발가이드 안내페이지에 표기할 게이트웨이 base URL (예: https://openapi.jejubank.co.kr)"));
}
private String resolve(String key, String defaultValue, String description) {
String value = portalPropertyService.getOrCreateProperty(GROUP, key, defaultValue, description);
return StringUtils.hasText(value) ? value.trim() : defaultValue;
}
private static String stripTrailingSlash(String s) {
if (s == null) {
return "";
}
return s.endsWith("/") ? s.substring(0, s.length() - 1) : s;
}
}