diff --git a/src/main/java/com/eactive/apim/portal/common/breadcrumb/GlobalControllerAdvice.java b/src/main/java/com/eactive/apim/portal/common/breadcrumb/GlobalControllerAdvice.java index 97d1b63..6645606 100644 --- a/src/main/java/com/eactive/apim/portal/common/breadcrumb/GlobalControllerAdvice.java +++ b/src/main/java/com/eactive/apim/portal/common/breadcrumb/GlobalControllerAdvice.java @@ -14,6 +14,7 @@ import com.eactive.apim.portal.apps.session.service.UserSessionService; import com.eactive.apim.portal.common.security.ClientGuardService; import com.eactive.apim.portal.djb.footer.RelatedSite; import com.eactive.apim.portal.djb.footer.RelatedSiteService; +import com.eactive.apim.portal.djb.guide.GuideProperty; import com.eactive.apim.portal.portalproperty.service.PortalPropertyService; @ControllerAdvice @@ -40,6 +41,9 @@ public class GlobalControllerAdvice { @Autowired private RelatedSiteService relatedSiteService; + @Autowired + private GuideProperty guideProperty; + @Autowired private Environment environment; @@ -133,4 +137,19 @@ public class GlobalControllerAdvice { public List relatedSites() { return relatedSiteService.getSites(); } + + /** + * 개발가이드 안내페이지에 표기할 게이트웨이 base URL. + * PortalProperty(Portal/djb.guide.gw-url)에서 조회하며, 환경별로 값이 달라진다. + * + *

가이드 페이지(/service/*-guide)에서만 필요하므로 다른 요청에서는 DB 조회 없이 null 을 반환한다. + */ + @ModelAttribute("guideGwUrl") + public String guideGwUrl(HttpServletRequest request) { + String uri = request.getRequestURI(); + if (uri == null || !uri.endsWith("-guide")) { + return null; + } + return guideProperty.gatewayUrl(); + } } diff --git a/src/main/java/com/eactive/apim/portal/djb/testbed/dto/DjbTestbedContextDto.java b/src/main/java/com/eactive/apim/portal/djb/testbed/dto/DjbTestbedContextDto.java index db3c3c1..b9d9a09 100644 --- a/src/main/java/com/eactive/apim/portal/djb/testbed/dto/DjbTestbedContextDto.java +++ b/src/main/java/com/eactive/apim/portal/djb/testbed/dto/DjbTestbedContextDto.java @@ -32,5 +32,11 @@ public class DjbTestbedContextDto { private String tokenUrl; private String apiKeyHeader; private String oauthTokenHeader; + + /** + * sample/mock 응답유형 API 에서 실제 토큰 발급 없이 주입할 고정 액세스 토큰. + * (실호출이 없거나 mock 서버로만 나가므로 검증 대상이 아니다 — {@code ApiTesterFilter} 의 mock 토큰과 동일 값) + */ + private String mockAccessToken; } } diff --git a/src/main/java/com/eactive/apim/portal/djb/testbed/service/DjbTestbedAuthService.java b/src/main/java/com/eactive/apim/portal/djb/testbed/service/DjbTestbedAuthService.java index 18a30d4..6bd81a8 100644 --- a/src/main/java/com/eactive/apim/portal/djb/testbed/service/DjbTestbedAuthService.java +++ b/src/main/java/com/eactive/apim/portal/djb/testbed/service/DjbTestbedAuthService.java @@ -119,6 +119,7 @@ public class DjbTestbedAuthService { .tokenUrl(gatewayProperty.resolveTokenUrl(portalOrigin)) .apiKeyHeader(gatewayProperty.apiKeyHeader()) .oauthTokenHeader(gatewayProperty.oauthHeader()) + .mockAccessToken(gatewayProperty.mockAccessToken()) .build(); } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 9b819f8..24b95e2 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -42,4 +42,10 @@ portal: # auth-virtual-code: 654321 dev: # application.yml의 `page:` 트리(브레드크럼/메뉴 이름) 라이브 반영 - hot-reload-pages: true \ No newline at end of file + hot-reload-pages: true + +server: + # 앞단 프록시(OHS 등) 경유 환경. ForwardedHeaderFilter 가 scheme/host/port/remoteAddr 을 교정한다. + # 상세 주석은 application.yml 의 동일 키 참고. /internal/** 은 필터에서 제외된다 + # (PortalConfigForwardedHeader — 원 소켓 IP 기반 허용 IP 검사를 보존하기 위함). + forward-headers-strategy: framework \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 9980d52..cc5d35e 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -32,4 +32,10 @@ app: resource-versioning: enabled: true resource-caching: - enabled: true \ No newline at end of file + enabled: true + +server: + # 앞단 프록시(OHS 등) 경유 환경. ForwardedHeaderFilter 가 scheme/host/port/remoteAddr 을 교정한다. + # 상세 주석은 application.yml 의 동일 키 참고. /internal/** 은 필터에서 제외된다 + # (PortalConfigForwardedHeader — 원 소켓 IP 기반 허용 IP 검사를 보존하기 위함). + forward-headers-strategy: framework \ No newline at end of file diff --git a/src/main/resources/application-stage.yml b/src/main/resources/application-stage.yml index 511c4b5..86f638c 100644 --- a/src/main/resources/application-stage.yml +++ b/src/main/resources/application-stage.yml @@ -47,3 +47,9 @@ gateway: hibernate-physical-naming-strategy: com.eactive.apim.portal.common.entity.CustomPhysicalNamingStrategy entity-package: com.eactive.eai.data.entity.onl,com.eactive.apim.gateway.data.statistics.entity +server: + # 앞단 프록시(OHS 등) 경유 환경. ForwardedHeaderFilter 가 scheme/host/port/remoteAddr 을 교정한다. + # 상세 주석은 application.yml 의 동일 키 참고. /internal/** 은 필터에서 제외된다 + # (PortalConfigForwardedHeader — 원 소켓 IP 기반 허용 IP 검사를 보존하기 위함). + forward-headers-strategy: framework + diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 99312fc..dd2d8cd 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -19,6 +19,15 @@ server: whitelabel: enabled: false path: /error + # 리버스프록시(OHS) 전달 헤더(X-Forwarded-*)를 요청에 반영하는 전략. + # native = 내장 Tomcat RemoteIpValve 위임 → WAR(WebLogic) 배포에서는 동작하지 않는다. + # framework = Spring ForwardedHeaderFilter(서블릿 필터) → 배포 방식과 무관하게 동작한다. + # 기본값 native: 로컬 개발은 gradle bootRun(내장 Tomcat)으로 기동하므로 컨테이너 기본 처리에 맡긴다. + # RemoteIpValve 는 internalProxies(사설/loopback 대역) 에서 온 요청의 헤더만 신뢰하므로 로컬에서 안전하다. + # 프록시 뒤에 놓이는 WAR 배포 환경(dev/stage/prod)만 각 프로파일에서 framework 로 올린다. + # ※ framework 로 올리는 환경은 OHS 에서 반드시 아래를 수행해야 한다(신뢰 프록시 목록이 없어 헤더를 무조건 신뢰). + # RequestHeader unset X-Forwarded-For / -Proto / -Host / -Port / Forwarded (클라이언트 위조분 제거) + # RequestHeader set X-Forwarded-Proto "https" / X-Forwarded-Port "1443" (mod_wl_ohs 는 미전송) forward-headers-strategy: native diff --git a/src/main/resources/templates/views/apps/apis/mainApiDetail.html b/src/main/resources/templates/views/apps/apis/mainApiDetail.html index 2301000..bba8987 100644 --- a/src/main/resources/templates/views/apps/apis/mainApiDetail.html +++ b/src/main/resources/templates/views/apps/apis/mainApiDetail.html @@ -647,16 +647,23 @@ return (CURRENT_RESPONSE_TYPE || '').toLowerCase() === 'mock'; } + // sample/mock 응답유형은 실제 인증이 성립하지 않는다. + // - sample: ApiTesterFilter 가 저장된 샘플 응답을 그대로 반환(실호출 없음) + // - mock : mock 서버로만 forward 되고 토큰도 고정값 즉시 발급(Secret 검증 없음) + // → 토큰 발급 왕복·Secret 입력 없이 바로 인증 상태로 진행한다. + // (responseType 미지정은 필터와 동일하게 sample 로 간주) + function isNoAuthApi() { + const type = (CURRENT_RESPONSE_TYPE || '').toLowerCase(); + return type === '' || type === 'sample' || type === 'mock'; + } + function authorizeApp(secret) { const gw = window.__djbGateway || {}; if (!window.ui) return; - // 응답유형 mock: 실 Secret 검증 없이 샘플 SecretKey + mock 토큰으로 인증 진행 — toast 안내 - if (isMockApi()) { - if (typeof djbToast === 'function') { - djbToast('Mock 서버로 호출되므로 별도 Secret Key 로 인증 절차를 진행합니다.', { type: 'info', duration: 5000 }); - } - proceedAuthorize(secret, secret.clientSecret || 'mock-secret', gw); + // sample/mock: 실제 인증 절차 생략하고 고정 토큰/키를 즉시 주입 — toast 안내 + if (isNoAuthApi()) { + authorizeWithoutAuth(secret, gw); return; } @@ -669,6 +676,34 @@ proceedAuthorize(secret, secret.clientSecret, gw); } + // sample/mock 전용 — 토큰 발급/Secret 입력 없이 Swagger authorize 에 바로 주입. + // 토큰 값은 컨텍스트의 mockAccessToken(= ApiTesterFilter 가 발급하는 값과 동일). + function authorizeWithoutAuth(secret, gw) { + if (typeof djbToast === 'function') { + djbToast(isMockApi() + ? 'Mock 응답 API 는 별도 인증 절차 없이 바로 호출됩니다.' + : '샘플 응답 API 는 별도 인증 절차 없이 바로 호출됩니다.', { type: 'info', duration: 5000 }); + } + if (secret.authType === 'OAUTH') { + window.ui.authActions.authorize({ + djbOAuth: { + name: 'djbOAuth', + schema: { type: 'apiKey', in: 'header', name: gw.oauthTokenHeader }, + value: 'Bearer ' + (gw.mockAccessToken || 'sample-access-token') + } + }); + } else if (secret.authType === 'API_KEY') { + window.ui.authActions.authorize({ + djbApiKey: { + name: 'djbApiKey', + schema: { type: 'apiKey', in: 'header', name: gw.apiKeyHeader }, + value: secret.clientSecret || gw.mockAccessToken || 'sample-api-key' + } + }); + } + refreshSnippetPanel(); + } + // Client Secret 수동 입력 팝업 (포탈에는 이미 삭제된 경우) function promptClientSecret(secret, gw) { if (typeof customPopups === 'undefined' || !customPopups.showPasswordInput) { diff --git a/src/main/resources/templates/views/apps/service/oauth2-guide.html b/src/main/resources/templates/views/apps/service/oauth2-guide.html index 533c9eb..9ea4136 100644 --- a/src/main/resources/templates/views/apps/service/oauth2-guide.html +++ b/src/main/resources/templates/views/apps/service/oauth2-guide.html @@ -169,8 +169,8 @@

POST - https://openapi.djbank.co.kr/dj/oauth/token + https://openapi.jejubank.co.kr/dj/oauth/token x-www-form-urlencoded
@@ -223,7 +223,7 @@
cURL
# 액세스 토큰 발급
-curl -X POST 'https://openapi.djbank.co.kr/dj/oauth/token' \
+curl -X POST '[[${guideGwUrl}]]/dj/oauth/token' \
   -H 'Content-Type: application/x-www-form-urlencoded' \
   -d 'grant_type=client_credentials' \
   -d 'client_id=YOUR_CLIENT_ID' \
@@ -323,7 +323,7 @@ curl -X POST 'https://openapi
                                 cURL
                                 
# 보호 자원 API 호출
 curl -X GET \
-  'https://openapi.djbank.co.kr/api/v1/accounts' \
+  '[[${guideGwUrl}]]/api/v1/accounts' \
   -H 'X-AUTH-TOKEN: Bearer eyJhbGciOiJSUzI..' \
   -H 'Accept: application/json'
 
diff --git a/src/main/resources/templates/views/apps/service/webhook-dev-guide.html b/src/main/resources/templates/views/apps/service/webhook-dev-guide.html
index 9331467..54383df 100644
--- a/src/main/resources/templates/views/apps/service/webhook-dev-guide.html
+++ b/src/main/resources/templates/views/apps/service/webhook-dev-guide.html
@@ -200,7 +200,7 @@
                                         
                                         
                                             X-Webhook-Event
-                                            이벤트 코드 (예: CONTROL_START)
+                                            이벤트 코드 (예: CHECK_START)
                                         
                                         
                                             X-Webhook-Timestamp
@@ -219,7 +219,7 @@
                             
Request Body · JSON
{
-  "eventType": "CONTROL_START",
+  "eventType": "CHECK_START",
   "eventId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
   "timestamp": 1723600000000,
   "data": [ "TESTCASE003S1", "TESTCASE005S1" ]
@@ -341,11 +341,11 @@ valid    = constantTimeEquals(received, expected)
- CONTROL_START + CHECK_START 점검 시작 - CONTROL_END + CHECK_END 점검 종료 @@ -414,7 +414,6 @@ valid = constantTimeEquals(received, expected)

⚠ 5xx 응답·타임아웃과 네트워크 오류는 일정 간격을 두고 재시도됩니다(기본 3회). 재시도로 인한 중복 수신은 eventId 멱등 처리로 방어하세요.

- >>>>>>> 9981459691c836bfd33b6b81a8e6aa22ca12446a

응답 가이드

    @@ -434,7 +433,7 @@ Content-Type: application/json { "result": "OK", - "eventType": "CONTROL_START" + "eventType": "CHECK_START" } # 서명 검증 실패