diff --git a/src/main/java/com/eactive/eai/rms/onl/apim/portalmenu/PortalMenuCacheClient.java b/src/main/java/com/eactive/eai/rms/onl/apim/portalmenu/PortalMenuCacheClient.java index c0565d4..5d7fc1f 100644 --- a/src/main/java/com/eactive/eai/rms/onl/apim/portalmenu/PortalMenuCacheClient.java +++ b/src/main/java/com/eactive/eai/rms/onl/apim/portalmenu/PortalMenuCacheClient.java @@ -1,11 +1,15 @@ package com.eactive.eai.rms.onl.apim.portalmenu; +import com.eactive.apim.portal.common.internal.InternalApiTokenService; import com.eactive.apim.portal.portalproperty.service.PortalPropertyService; import org.apache.http.client.HttpClient; import org.apache.http.client.config.RequestConfig; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.stereotype.Component; @@ -33,8 +37,12 @@ import java.util.concurrent.TimeUnit; * (기본 {@code http://127.0.0.1:39130/internal/menu/reload}). * 포탈이 다중 인스턴스로 기동되는 환경을 위해 콤마/세미콜론/줄바꿈 으로 여러 URL 을 나열할 수 있으며, * 나열된 모든 서버로 병렬 호출한다. 각 호출의 connect/read 타임아웃은 {@value #TIMEOUT_MS}ms 이다. - * portal 측은 {@code menu.internal.allow-ips} 허용 IP 목록으로 요청을 검증한다. * 오류는 결과 Map 으로 감싸 화면에 서버별 사유를 표시한다 — 예외를 전파하지 않는다.

+ * + *

portal 측 검증은 두 겹이다: {@code menu.internal.allow-ips} 허용 IP 목록과 + * {@link InternalApiTokenService} 공유 토큰 헤더. 토큰/헤더명은 PTL_PROPERTY 로 portal 과 공유하므로 + * 이 클래스는 읽기만 한다(생성 주체는 portal). 값이 아직 없으면 헤더 없이 호출하게 되어 portal 이 + * 401 로 거부하며, 이때는 portal 을 먼저 기동해 토큰을 생성해야 한다.

*/ @Component public class PortalMenuCacheClient { @@ -55,16 +63,21 @@ public class PortalMenuCacheClient { private static final int MAX_PARALLELISM = 10; private final PortalPropertyService portalPropertyService; + private final InternalApiTokenService internalApiTokenService; private final RestTemplate restTemplate; @Autowired - public PortalMenuCacheClient(PortalPropertyService portalPropertyService) { - this(portalPropertyService, createRestTemplateWithTimeout(TIMEOUT_MS)); + public PortalMenuCacheClient(PortalPropertyService portalPropertyService, + InternalApiTokenService internalApiTokenService) { + this(portalPropertyService, internalApiTokenService, createRestTemplateWithTimeout(TIMEOUT_MS)); } /** 테스트용 — 타임아웃이 설정된 RestTemplate 을 직접 주입한다. */ - PortalMenuCacheClient(PortalPropertyService portalPropertyService, RestTemplate restTemplate) { + PortalMenuCacheClient(PortalPropertyService portalPropertyService, + InternalApiTokenService internalApiTokenService, + RestTemplate restTemplate) { this.portalPropertyService = portalPropertyService; + this.internalApiTokenService = internalApiTokenService; this.restTemplate = restTemplate; } @@ -98,7 +111,8 @@ public class PortalMenuCacheClient { */ public Map reload() { List urls = resolveUrls(); - List> servers = callAll(urls); + // 토큰/헤더명 조회는 리로드 1회당 1번만 (서버 수만큼 DB 를 때리지 않는다) + List> servers = callAll(urls, buildRequest()); int successCount = 0; for (Map server : servers) { @@ -140,13 +154,28 @@ public class PortalMenuCacheClient { return "포탈 메뉴 캐시 리로드 일부 실패 (" + successCount + "/" + total + " 서버 성공)"; } + /** + * 내부 API 인증 헤더를 실은 빈 본문 요청. 토큰이 아직 없으면 헤더 없이 나가고 포탈이 401 로 거부한다. + */ + private HttpEntity buildRequest() { + HttpHeaders headers = new HttpHeaders(); + String token = internalApiTokenService.findToken(); + if (token == null) { + logger.warn("포탈 내부 API 토큰 없음 - PTL_PROPERTY " + InternalApiTokenService.PROP_GROUP + "/" + + InternalApiTokenService.PROP_TOKEN + " (포탈 최초 기동 시 자동 생성)"); + } else { + headers.set(internalApiTokenService.findHeaderName(), token); + } + return new HttpEntity(headers); + } + /** 모든 URL 로 병렬 호출. 반환 순서는 {@code urls} 순서와 동일하다. */ - private List> callAll(List urls) { + private List> callAll(List urls, final HttpEntity request) { if (urls.isEmpty()) { return Collections.emptyList(); } if (urls.size() == 1) { - return Collections.singletonList(callOne(urls.get(0))); + return Collections.singletonList(callOne(urls.get(0), request)); } List>> tasks = new ArrayList<>(urls.size()); @@ -154,7 +183,7 @@ public class PortalMenuCacheClient { tasks.add(new Callable>() { @Override public Map call() { - return callOne(url); + return callOne(url, request); } }); } @@ -193,11 +222,11 @@ public class PortalMenuCacheClient { } /** 서버 1대 호출 — 예외를 전파하지 않고 실패 Map 으로 감싼다. */ - private Map callOne(String url) { + private Map callOne(String url, HttpEntity request) { long startedAt = System.currentTimeMillis(); try { - @SuppressWarnings("unchecked") - ResponseEntity response = restTemplate.postForEntity(url, null, Map.class); + @SuppressWarnings("rawtypes") + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, request, Map.class); long elapsed = System.currentTimeMillis() - startedAt; @SuppressWarnings("unchecked")