개발가이드 Base URL 동적 처리, Mock 응답 인증 로직 추가:
eapim-portal CI / build (push) Has been cancelled
eapim-portal Test / test (push) Has been cancelled

- GuideProperty 이용 개발가이드 GW URL 동적 주입
- sample/mock 응답 유형의 인증 절차 생략 로직 추가
- Webhook 이벤트 코드명/JSON 값 수정
This commit is contained in:
Rinjae
2026-08-11 11:46:49 +09:00
parent 63d4d7268c
commit 766341540d
10 changed files with 105 additions and 18 deletions
@@ -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<RelatedSite> relatedSites() {
return relatedSiteService.getSites();
}
/**
* 개발가이드 안내페이지에 표기할 게이트웨이 base URL.
* PortalProperty(Portal/djb.guide.gw-url)에서 조회하며, 환경별로 값이 달라진다.
*
* <p>가이드 페이지(/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();
}
}
@@ -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;
}
}
@@ -119,6 +119,7 @@ public class DjbTestbedAuthService {
.tokenUrl(gatewayProperty.resolveTokenUrl(portalOrigin))
.apiKeyHeader(gatewayProperty.apiKeyHeader())
.oauthTokenHeader(gatewayProperty.oauthHeader())
.mockAccessToken(gatewayProperty.mockAccessToken())
.build();
}
+7 -1
View File
@@ -42,4 +42,10 @@ portal:
# auth-virtual-code: 654321
dev:
# application.yml의 `page:` 트리(브레드크럼/메뉴 이름) 라이브 반영
hot-reload-pages: true
hot-reload-pages: true
server:
# 앞단 프록시(OHS 등) 경유 환경. ForwardedHeaderFilter 가 scheme/host/port/remoteAddr 을 교정한다.
# 상세 주석은 application.yml 의 동일 키 참고. /internal/** 은 필터에서 제외된다
# (PortalConfigForwardedHeader — 원 소켓 IP 기반 허용 IP 검사를 보존하기 위함).
forward-headers-strategy: framework
+7 -1
View File
@@ -32,4 +32,10 @@ app:
resource-versioning:
enabled: true
resource-caching:
enabled: true
enabled: true
server:
# 앞단 프록시(OHS 등) 경유 환경. ForwardedHeaderFilter 가 scheme/host/port/remoteAddr 을 교정한다.
# 상세 주석은 application.yml 의 동일 키 참고. /internal/** 은 필터에서 제외된다
# (PortalConfigForwardedHeader — 원 소켓 IP 기반 허용 IP 검사를 보존하기 위함).
forward-headers-strategy: framework
+6
View File
@@ -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
+9
View File
@@ -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
@@ -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) {
@@ -169,8 +169,8 @@
<div class="oauth2-2legged__endpoint-box">
<span class="oauth2-2legged__method">POST</span>
<code
class="oauth2-2legged__endpoint-path">https://openapi.djbank.co.kr/dj/oauth/token</code>
<code class="oauth2-2legged__endpoint-path"
th:text="|${guideGwUrl}/dj/oauth/token|">https://openapi.jejubank.co.kr/dj/oauth/token</code>
<span class="oauth2-2legged__endpoint-content-type">x-www-form-urlencoded</span>
</div>
@@ -223,7 +223,7 @@
<div class="oauth2-2legged__code-panel">
<span class="oauth2-2legged__code-tag">cURL</span>
<pre class="oauth2-2legged__code-block"><span class="o2leg-cm"># 액세스 토큰 발급</span>
curl -X <span class="o2leg-y">POST</span> <span class="o2leg-c">'https://openapi.djbank.co.kr/dj/oauth/token'</span> \
curl -X <span class="o2leg-y">POST</span> <span class="o2leg-c">'[[${guideGwUrl}]]/dj/oauth/token'</span> \
-H <span class="o2leg-c">'Content-Type: application/x-www-form-urlencoded'</span> \
-d <span class="o2leg-c">'grant_type=client_credentials'</span> \
-d <span class="o2leg-c">'client_id=YOUR_CLIENT_ID'</span> \
@@ -323,7 +323,7 @@ curl -X <span class="o2leg-y">POST</span> <span class="o2leg-c">'https://openapi
<span class="oauth2-2legged__code-tag">cURL</span>
<pre class="oauth2-2legged__code-block"><span class="o2leg-cm"># 보호 자원 API 호출</span>
curl -X <span class="o2leg-y">GET</span> \
<span class="o2leg-c">'https://openapi.djbank.co.kr/api/v1/accounts'</span> \
<span class="o2leg-c">'[[${guideGwUrl}]]/api/v1/accounts'</span> \
-H <span class="o2leg-c">'X-AUTH-TOKEN: Bearer eyJhbGciOiJSUzI..'</span> \
-H <span class="o2leg-c">'Accept: application/json'</span>
@@ -200,7 +200,7 @@
</tr>
<tr>
<td><code>X-Webhook-Event</code></td>
<td>이벤트 코드 (예: CONTROL_START)</td>
<td>이벤트 코드 (예: CHECK_START)</td>
</tr>
<tr>
<td><code>X-Webhook-Timestamp</code></td>
@@ -219,7 +219,7 @@
<div class="oauth2-2legged__code-panel">
<span class="oauth2-2legged__code-tag">Request Body · JSON</span>
<pre class="oauth2-2legged__code-block">{
<span class="o2leg-c">"eventType"</span>: <span class="o2leg-y">"CONTROL_START"</span>,
<span class="o2leg-c">"eventType"</span>: <span class="o2leg-y">"CHECK_START"</span>,
<span class="o2leg-c">"eventId"</span>: <span class="o2leg-y">"f47ac10b-58cc-4372-a567-0e02b2c3d479"</span>,
<span class="o2leg-c">"timestamp"</span>: <span class="o2leg-p">1723600000000</span>,
<span class="o2leg-c">"data"</span>: [ <span class="o2leg-y">"TESTCASE003S1"</span>, <span class="o2leg-y">"TESTCASE005S1"</span> ]
@@ -341,11 +341,11 @@ valid = constantTimeEquals(received, expected)</pre>
</thead>
<tbody>
<tr>
<td><code>CONTROL_START</code></td>
<td><code>CHECK_START</code></td>
<td>점검 시작</td>
</tr>
<tr>
<td><code>CONTROL_END</code></td>
<td><code>CHECK_END</code></td>
<td>점검 종료</td>
</tr>
<tr>
@@ -414,7 +414,6 @@ valid = constantTimeEquals(received, expected)</pre>
</table>
<p class="oauth2-2legged__warning">⚠ 5xx 응답·타임아웃과 네트워크 오류는 <strong>일정 간격을 두고
재시도</strong>됩니다(기본 3회). 재시도로 인한 중복 수신은 <code>eventId</code> 멱등 처리로 방어하세요.</p>
>>>>>>> 9981459691c836bfd33b6b81a8e6aa22ca12446a
<h4 class="oauth2-2legged__panel-subtitle">응답 가이드</h4>
<ul class="oauth2-2legged__tips">
@@ -434,7 +433,7 @@ Content-Type: application/json
{
<span class="o2leg-c">"result"</span>: <span class="o2leg-y">"OK"</span>,
<span class="o2leg-c">"eventType"</span>: <span class="o2leg-y">"CONTROL_START"</span>
<span class="o2leg-c">"eventType"</span>: <span class="o2leg-y">"CHECK_START"</span>
}
<span class="o2leg-cm"># 서명 검증 실패</span>