Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d41170b67 |
+9
-51
@@ -25,8 +25,7 @@ java {
|
||||
|
||||
compileJava {
|
||||
options.encoding = 'UTF-8'
|
||||
// generatedJavaDir 는 아래 generatedSourceOutputDirectory 로 APT 가 이미 컴파일함.
|
||||
// srcDir 로도 등록하면 낡은 생성물이 입력소스가 돼 APT 재생성 시 duplicate class 발생 → 등록 금지.
|
||||
sourceSets.main.java { srcDir generatedJavaDir }
|
||||
options.generatedSourceOutputDirectory = project.file(generatedJavaDir)
|
||||
|
||||
aptOptions {
|
||||
@@ -51,16 +50,8 @@ dependencies {
|
||||
//implementation project(':elink-online-transformer')
|
||||
api project(':elink-online-transformer')
|
||||
|
||||
// damo-manager.jar 를 제외한 나머지 libs 는 기존대로 컴파일 시점에만 사용한다.
|
||||
// (WAS lib 또는 다른 경로에서 런타임에 제공됨)
|
||||
compileOnly fileTree(dir: 'libs', include: ['*.jar'], exclude: ['damo-manager.jar'])
|
||||
|
||||
// damo-manager.jar: 기존에는 tomcat/lib 에 직접 넣어 런타임에 제공했으나,
|
||||
// 배포 WAR(eapim-online.war)의 WEB-INF/lib 에 포함시키기 위해 런타임 의존성으로 전환한다.
|
||||
// implementation 이므로 이 모듈의 compile/test classpath 와, 이 모듈을 project 의존성으로
|
||||
// 참조하는 eapim-online 루트의 runtimeClasspath(= war 패키징 대상)에 함께 포함된다.
|
||||
implementation files('libs/damo-manager.jar')
|
||||
|
||||
compileOnly fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
api (group: 'org.apache.activemq', name: 'activemq-console', version: '5.14.5'){
|
||||
exclude group: 'com.fasterxml.jackson.core'
|
||||
}
|
||||
@@ -75,11 +66,7 @@ dependencies {
|
||||
api 'io.micrometer:micrometer-core:1.5.17'
|
||||
api 'io.micrometer:micrometer-registry-prometheus:1.5.17'
|
||||
|
||||
// gson 2.3.1 -> 2.8.9 (CVE-2022-25647: 악의적 데이터 역직렬화 시 메모리 고갈/DoS)
|
||||
// 사용처는 Jsons(new Gson), AlarmService(fromJson), TemplateAdapterErrorMsgHandler(JsonParser),
|
||||
// alarm/ums/payload/*(@SerializedName) 4곳뿐이고 사용 API 는 그대로 유지된다.
|
||||
// (new JsonParser().parse() 는 2.8.9 에서 deprecated 이지만 제거되지 않아 동작에 영향 없음)
|
||||
implementation "com.google.code.gson:gson:2.8.9"
|
||||
implementation "com.google.code.gson:gson:2.3.1"
|
||||
|
||||
//api "com.eactive:mina-core-1.0.10-custom:1.0:custom@jar"
|
||||
api ("org.apache.mina:mina-filter-ssl:1.0.10") {
|
||||
@@ -95,27 +82,12 @@ dependencies {
|
||||
|
||||
api 'com.nimbusds:nimbus-jose-jwt:9.24.3'
|
||||
|
||||
// BouncyCastle: bcprov-jdk15on 라인은 1.70에서 종료되어 후속 패치가 없다.
|
||||
// jdk18on 라인으로 전환한다(패키지명·프로바이더명("BC") 동일 → 소스 변경 없음,
|
||||
// 클래스파일 major 52 / Bundle-RequiredExecutionEnvironment: JavaSE-1.8 이라 JDK 8 유지 가능).
|
||||
// 사용처: AESCryptoModuleExtension, ARIACryptoModuleExtension
|
||||
// (BouncyCastleProvider 등록, ARIA 알고리즘, FPE 모드의 FPEParameterSpec)
|
||||
// 1.70 대비 CVE-2023-33201 / CVE-2024-29857 / CVE-2024-30171 / CVE-2024-30172 해소.
|
||||
// bcpkix 는 spring-security-jwt 가 전이로 끌고 오던 1.64 를 대체한다(아래 exclude 참조).
|
||||
api 'org.bouncycastle:bcprov-jdk18on:1.78.1'
|
||||
api 'org.bouncycastle:bcpkix-jdk18on:1.78.1'
|
||||
|
||||
api "io.undertow:undertow-servlet:${undertowVersion}"
|
||||
// Java-WebSocket 1.3.9 -> 1.5.7 (CVE-2020-11050: 구형 암호화 통신/인증서 검증 결함에 의한 MitM)
|
||||
// 사용 API 는 WebSocketServer 상속(onOpen/onClose/onMessage/onError/onStart)과
|
||||
// WebSocket 의 send/close/getRemoteSocketAddress/isOpen, start()/stop(timeout) 뿐이라
|
||||
// 1.4.0 의 파괴적 변경(WebSocketImpl.DEBUG 제거, Draft_10/17 제거, connections() -> getConnections())
|
||||
// 에 해당하는 사용처가 없다. 1.5.x 는 로깅을 SLF4J 로 하는데 이미 클래스패스에 있다.
|
||||
api 'org.java-websocket:Java-WebSocket:1.5.7'
|
||||
api 'org.java-websocket:Java-WebSocket:1.3.9'
|
||||
api 'javax.cache:cache-api:1.1.1'
|
||||
|
||||
api 'org.apache.ignite:ignite-slf4j:2.14.0'
|
||||
api 'org.apache.ignite:ignite-kubernetes:2.14.0'
|
||||
api 'org.apache.ignite:ignite-slf4j:2.16.0'
|
||||
api 'org.apache.ignite:ignite-kubernetes:2.16.0'
|
||||
// ignite 2.17 - JDK 11 이상 필요
|
||||
// api 'org.apache.ignite:ignite-slf4j:2.17.0'
|
||||
// api 'org.apache.ignite:ignite-kubernetes:2.17.0'
|
||||
@@ -158,28 +130,14 @@ dependencies {
|
||||
compileOnly group: 'javax.servlet.jsp', name: 'javax.servlet.jsp-api', version: '2.3.3'
|
||||
compileOnly 'javax.resource:javax.resource-api:1.7'
|
||||
compileOnly 'javax.jms:javax.jms-api:2.0.1'
|
||||
|
||||
// jackson-dataformat-xml 제거 (2026-08-27)
|
||||
// XmlMapper/JacksonXml* 사용처가 전 소스에 0건이고, 선언 버전(2.13.1)이
|
||||
// 실제 해석되는 jackson-core/databind(2.12.7)와 마이너 불일치라 승격 시 위험했다.
|
||||
|
||||
// bcpkix-jdk15on:1.64 -> bcprov-jdk15on:1.64 를 전이로 끌고 온다.
|
||||
// 위에서 jdk18on 으로 전환했으므로 함께 두면 org.bouncycastle.* 클래스가 중복되고
|
||||
// 로딩 순서에 따라 구버전이 선택될 수 있다. 전이를 끊고 jdk18on 만 사용한다.
|
||||
// (이 exclude 는 발행 POM 에도 기록되어 이 모듈을 참조하는 타 사이트에도 동일 적용된다)
|
||||
api ("org.springframework.security:spring-security-jwt:1.1.1.RELEASE") {
|
||||
exclude group: 'org.bouncycastle'
|
||||
}
|
||||
|
||||
compileOnly group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.13.1'
|
||||
|
||||
testRuntimeOnly 'com.h2database:h2:2.1.214'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
|
||||
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test:2.6.15'
|
||||
testImplementation 'junit:junit:4.4'
|
||||
testImplementation files('libs/kjb-safedb.jar')
|
||||
// libs 는 main 에서 compileOnly 라 테스트 클래스패스에 오르지 않는다.
|
||||
// JsonToSetStatusFilterTest 가 org.json.simple.JSONObject 를 쓰므로 테스트에만 추가한다.
|
||||
testImplementation files('libs/json-simple-1.1.1-custom-1.2.jar')
|
||||
}
|
||||
|
||||
test {
|
||||
|
||||
Binary file not shown.
@@ -1,391 +0,0 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "b7e2e1d0-3c1a-4b0a-9d2e-crypto-manage-0001",
|
||||
"name": "CryptoModuleManage API (/manage/crypto)",
|
||||
"description": "CryptoModuleManager 기반 암호화모듈 진단 및 암복호화 테스트 API 샘플 모음.\n\n사용 전 확인사항:\n1. Collection Variables 의 baseUrl 을 실제 서버 주소로 변경\n2. cryptoName / dynamicCryptoName 을 DB(crypto_module_config)에 실제 등록된 모듈명으로 변경\n3. DYNAMIC 모듈은 keyDerivParams 의 contextKey 값과 dynamicContextKey 변수가 일치해야 함 (예: X-Api-Enc-Key)\n\n주의: 이 API는 원본 암호키(encKeyHex/decKeyHex/ivHex)를 절대 응답에 포함하지 않는다. 진단 응답은 메타데이터와 키 도출 전략(KeyDerivationStrategy) 로드 상태만 노출한다.",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"variable": [
|
||||
{ "key": "baseUrl", "value": "http://localhost:8080", "type": "string" },
|
||||
{ "key": "cryptoName", "value": "SAMPLE_STATIC", "type": "string", "description": "STATIC 키 방식으로 등록된 실제 cryptoName으로 교체" },
|
||||
{ "key": "dynamicCryptoName", "value": "SAMPLE_DYNAMIC", "type": "string", "description": "DYNAMIC 키 방식으로 등록된 실제 cryptoName으로 교체" },
|
||||
{ "key": "dynamicContextKey", "value": "X-Api-Enc-Key", "type": "string", "description": "DYNAMIC 모듈의 key_deriv_params.contextKey 값과 일치해야 함" },
|
||||
{ "key": "dynamicContextValue", "value": "clientKeyValue01", "type": "string" },
|
||||
{ "key": "plainText", "value": "테스트 평문입니다", "type": "string" },
|
||||
{ "key": "plainTextBase64", "value": "", "type": "string" },
|
||||
{ "key": "staticCipherTextBase64", "value": "", "type": "string" },
|
||||
{ "key": "dynamicCipherTextBase64", "value": "", "type": "string" }
|
||||
],
|
||||
"item": [
|
||||
{
|
||||
"name": "1. 진단 (Diagnostics)",
|
||||
"item": [
|
||||
{
|
||||
"name": "전체 암호화모듈 목록 조회",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/crypto/list",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "crypto", "list"]
|
||||
},
|
||||
"description": "등록된 전체 암호화모듈의 진단정보(메타데이터)를 반환한다. 원본 키는 포함되지 않는다."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('data는 배열', () => pm.expect(json.data).to.be.an('array'));",
|
||||
"pm.test('원본 키 필드가 응답에 없어야 함', () => {",
|
||||
" const body = pm.response.text();",
|
||||
" pm.expect(body).to.not.include('encKeyHex');",
|
||||
" pm.expect(body).to.not.include('decKeyHex');",
|
||||
"});"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "단일 암호화모듈 조회",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/crypto/{{cryptoName}}",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "crypto", "{{cryptoName}}"]
|
||||
},
|
||||
"description": "cryptoName 하나의 진단정보를 조회한다. STATIC 모듈은 keyDerivStrategy가 null, strategyLoaded가 false로 내려온다."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('cryptoName 일치', () => pm.expect(json.data.cryptoName).to.eql(pm.collectionVariables.get('cryptoName')));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "DYNAMIC 모듈 조회 (전략 로드 확인)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/crypto/{{dynamicCryptoName}}",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "crypto", "{{dynamicCryptoName}}"]
|
||||
},
|
||||
"description": "DYNAMIC 모듈은 resolveStrategy를 통해 키 도출 전략(KeyDerivationStrategy) 클래스가 정상 로드되는지 strategyLoaded/strategyClassName으로 확인할 수 있다."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('keySourceType = DYNAMIC', () => pm.expect(json.data.keySourceType).to.eql('DYNAMIC'));",
|
||||
"pm.test('strategyLoaded = true', () => pm.expect(json.data.strategyLoaded).to.eql(true));",
|
||||
"pm.test('strategyClassName 존재', () => pm.expect(json.data.strategyClassName).to.be.a('string'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "동적 키 캐시 목록 조회",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/crypto/cache/dynamic-keys",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "crypto", "cache", "dynamic-keys"]
|
||||
},
|
||||
"description": "DYNAMIC 키 방식 모듈이 캐시한 키의 캐시-키 목록(예: 'cryptoName:contextValue')을 반환한다. 원본 키 값이 아니다. 아래 'DYNAMIC 키' 폴더의 암호화 요청을 먼저 실행한 뒤 호출하면 목록이 채워진다."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('data는 배열', () => pm.expect(json.data).to.be.an('array'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "2. STATIC 키 암복호화 테스트",
|
||||
"item": [
|
||||
{
|
||||
"name": "암호화 (encrypt)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"plainTextBase64\": \"{{plainTextBase64}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/crypto/{{cryptoName}}/test/encrypt",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "crypto", "{{cryptoName}}", "test", "encrypt"]
|
||||
},
|
||||
"description": "STATIC 키 방식 모듈로 평문(Base64)을 암호화한다. runtimeContext 없이 호출한다."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "prerequest",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const plainText = pm.collectionVariables.get('plainText');",
|
||||
"pm.collectionVariables.set('plainTextBase64', btoa(unescape(encodeURIComponent(plainText))));"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('cipherTextBase64 존재', () => pm.expect(json.data.cipherTextBase64).to.be.a('string'));",
|
||||
"pm.test('평문 base64와 달라야 함', () => pm.expect(json.data.cipherTextBase64).to.not.eql(pm.collectionVariables.get('plainTextBase64')));",
|
||||
"pm.collectionVariables.set('staticCipherTextBase64', json.data.cipherTextBase64);"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "복호화 (decrypt) — 위 암호화 결과 사용",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"cipherTextBase64\": \"{{staticCipherTextBase64}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/crypto/{{cryptoName}}/test/decrypt",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "crypto", "{{cryptoName}}", "test", "decrypt"]
|
||||
},
|
||||
"description": "바로 위 '암호화 (encrypt)' 요청에서 저장한 staticCipherTextBase64를 복호화하여 원문과 일치하는지 확인한다. 반드시 암호화 요청을 먼저 실행할 것."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"const decoded = decodeURIComponent(escape(atob(json.data.plainTextBase64)));",
|
||||
"pm.test('원문과 일치 (라운드트립)', () => pm.expect(decoded).to.eql(pm.collectionVariables.get('plainText')));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "3. DYNAMIC 키 암복호화 테스트",
|
||||
"item": [
|
||||
{
|
||||
"name": "암호화 (encrypt, runtimeContext 포함)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"plainTextBase64\": \"{{plainTextBase64}}\",\n \"runtimeContext\": {\n \"{{dynamicContextKey}}\": \"{{dynamicContextValue}}\"\n }\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/crypto/{{dynamicCryptoName}}/test/encrypt",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "crypto", "{{dynamicCryptoName}}", "test", "encrypt"]
|
||||
},
|
||||
"description": "DYNAMIC 키 방식 모듈로 runtimeContext(예: 헤더값)를 전달하여 키를 도출한 뒤 암호화한다. dynamicContextKey는 DB에 등록된 key_deriv_params.contextKey와 일치해야 한다."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "prerequest",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const plainText = pm.collectionVariables.get('plainText');",
|
||||
"pm.collectionVariables.set('plainTextBase64', btoa(unescape(encodeURIComponent(plainText))));"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('cipherTextBase64 존재', () => pm.expect(json.data.cipherTextBase64).to.be.a('string'));",
|
||||
"pm.collectionVariables.set('dynamicCipherTextBase64', json.data.cipherTextBase64);"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "복호화 (decrypt, 동일 runtimeContext 필요)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"cipherTextBase64\": \"{{dynamicCipherTextBase64}}\",\n \"runtimeContext\": {\n \"{{dynamicContextKey}}\": \"{{dynamicContextValue}}\"\n }\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/crypto/{{dynamicCryptoName}}/test/decrypt",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "crypto", "{{dynamicCryptoName}}", "test", "decrypt"]
|
||||
},
|
||||
"description": "암호화 시 사용한 것과 동일한 runtimeContext를 전달해야 동일한 키가 도출되어 복호화에 성공한다. dynamicContextValue를 바꿔서 보내면 키가 달라져 복호화가 실패(예외)하는 것도 확인해볼 수 있다."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"const decoded = decodeURIComponent(escape(atob(json.data.plainTextBase64)));",
|
||||
"pm.test('원문과 일치 (라운드트립)', () => pm.expect(decoded).to.eql(pm.collectionVariables.get('plainText')));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "4. 에러 케이스",
|
||||
"item": [
|
||||
{
|
||||
"name": "미등록 cryptoName 조회 → success=false",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/crypto/NOT_REGISTERED_MODULE",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "crypto", "NOT_REGISTERED_MODULE"]
|
||||
},
|
||||
"description": "존재하지 않는 cryptoName 조회 시 HTTP 200 + success=false + message에 사유가 담겨 반환되는지 확인한다 (예외가 그대로 500으로 노출되지 않음)."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = false', () => pm.expect(json.success).to.eql(false));",
|
||||
"pm.test('message에 모듈명 포함', () => pm.expect(json.message).to.include('NOT_REGISTERED_MODULE'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "잘못된 Base64로 복호화 → success=false",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"cipherTextBase64\": \"not-valid-base64!!\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/crypto/{{cryptoName}}/test/decrypt",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "crypto", "{{cryptoName}}", "test", "decrypt"]
|
||||
},
|
||||
"description": "Base64 형식이 아닌 문자열을 전달하면 success=false로 처리되고 서버가 500으로 죽지 않는지 확인한다."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = false', () => pm.expect(json.success).to.eql(false));",
|
||||
"pm.test('message 존재', () => pm.expect(json.message).to.be.a('string'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "다른 runtimeContext로 DYNAMIC 복호화 시도 → 실패 확인",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"cipherTextBase64\": \"{{dynamicCipherTextBase64}}\",\n \"runtimeContext\": {\n \"{{dynamicContextKey}}\": \"differentContextValue\"\n }\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/crypto/{{dynamicCryptoName}}/test/decrypt",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "crypto", "{{dynamicCryptoName}}", "test", "decrypt"]
|
||||
},
|
||||
"description": "'3. DYNAMIC 키' 폴더의 암호화를 먼저 실행해 dynamicCipherTextBase64를 채운 뒤, 암호화 때와 다른 runtimeContext 값으로 복호화를 시도한다. 키가 달라 padding/복호화 오류로 success=false가 되는 것을 확인한다 (패딩 모드에 따라 우연히 성공할 수도 있으니 참고용)."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"console.log('다른 컨텍스트 복호화 결과:', JSON.stringify(json));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "c3f4a1e0-9b2d-4a11-8e5a-session-cache-0001",
|
||||
"name": "SessionCacheManage API (/manage/session)",
|
||||
"description": "SessionManager(SessionManagerForIgnite/SessionManagerForEhcache) 캐시 조회 API 샘플 모음.\n\n사용 전 확인사항:\n1. Collection Variables 의 baseUrl 을 실제 서버 주소로 변경\n2. 서버가 Ignite 캐시 백엔드로 기동된 경우에만 outbound-access-token 캐시가 채워짐 (Ehcache 백엔드는 success=false로 응답, 정상 동작임)\n3. 모든 요청은 조회 전용(GET)이며 실제 세션/캐시 데이터를 변경하지 않는다.",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"variable": [
|
||||
{ "key": "baseUrl", "value": "http://localhost:8080", "type": "string" }
|
||||
],
|
||||
"item": [
|
||||
{
|
||||
"name": "1. 캐시 요약 / 마스터 인스턴스",
|
||||
"item": [
|
||||
{
|
||||
"name": "전체 캐시 요약 (구현체별 XML)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/session/cache/status",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "session", "cache", "status"]
|
||||
},
|
||||
"description": "Ignite/Ehcache 구현체가 각자 형식으로 만든 전체 캐시 이름/크기 요약 텍스트(XML)를 반환한다."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('data는 문자열(XML)', () => pm.expect(json.data).to.be.a('string'));",
|
||||
"pm.test('Caches 루트 태그 포함', () => pm.expect(json.data).to.include('<Caches'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "현재 evict master 인스턴스 조회",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/session/master-inst-id",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "session", "master-inst-id"]
|
||||
},
|
||||
"description": "싱글 어댑터 처리를 담당하는 현재 master 서버 인스턴스명을 반환한다. 아직 선출되지 않았으면 data가 null일 수 있다."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"console.log('master instance id:', json.data);"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "2. 개별 캐시 조회",
|
||||
"item": [
|
||||
{
|
||||
"name": "로그인(WebSocket) 캐시",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/session/cache/login",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "session", "cache", "login"]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('name = loginSession', () => pm.expect(json.data.name).to.eql('loginSession'));",
|
||||
"pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));",
|
||||
"pm.test('size는 keys 개수와 일치', () => pm.expect(json.data.size).to.eql(json.data.keys.length));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "HTTP 로그인 캐시",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/session/cache/http-login",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "session", "cache", "http-login"]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('name = httpLogin', () => pm.expect(json.data.name).to.eql('httpLogin'));",
|
||||
"pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "싱글 어댑터 evict-master 캐시",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/session/cache/evict-master",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "session", "cache", "evict-master"]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('name = evictMasterCache', () => pm.expect(json.data.name).to.eql('evictMasterCache'));",
|
||||
"pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "단말(ATM 등) 캐시",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/session/cache/terminal",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "session", "cache", "terminal"]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('name = terminalSession', () => pm.expect(json.data.name).to.eql('terminalSession'));",
|
||||
"pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "단말ID→사용자ID 변환 캐시",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/session/cache/convert-user-id",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "session", "cache", "convert-user-id"]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('name = convertUserIdSession', () => pm.expect(json.data.name).to.eql('convertUserIdSession'));",
|
||||
"pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "소켓 세션 캐시",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/session/cache/socket",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "session", "cache", "socket"]
|
||||
}
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('name = adapterSession', () => pm.expect(json.data.name).to.eql('adapterSession'));",
|
||||
"pm.test('keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "아웃바운드 AccessToken 캐시 (Ignite 전용)",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/session/cache/outbound-access-token",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "session", "cache", "outbound-access-token"]
|
||||
},
|
||||
"description": "Ignite 백엔드에서만 지원한다. Ehcache 백엔드로 기동된 서버라면 success=false + message로 응답하며, 이는 정상 동작이다(서버 500 아님)."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"if (json.success) {",
|
||||
" pm.test('data.name 존재', () => pm.expect(json.data.name).to.be.a('string'));",
|
||||
" pm.test('data.keys는 배열', () => pm.expect(json.data.keys).to.be.an('array'));",
|
||||
"} else {",
|
||||
" pm.test('미지원 백엔드는 message 포함', () => pm.expect(json.message).to.be.a('string'));",
|
||||
" console.log('OutboundAccessToken 캐시 미지원 백엔드:', json.message);",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "3. 전체 캐시 목록",
|
||||
"item": [
|
||||
{
|
||||
"name": "모든 캐시 한번에 조회",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/session/cache/all",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "session", "cache", "all"]
|
||||
},
|
||||
"description": "login/http-login/evict-master/terminal/convert-user-id/socket 6종은 항상 포함되고, outbound-access-token은 Ignite 백엔드일 때만 추가되어 count가 6 또는 7이 된다."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('data는 배열', () => pm.expect(json.data).to.be.an('array'));",
|
||||
"pm.test('count는 6 또는 7 (백엔드에 따라 outbound-access-token 포함 여부 다름)', () => {",
|
||||
" pm.expect(json.count).to.be.oneOf([6, 7]);",
|
||||
"});",
|
||||
"pm.test('count와 data.length 일치', () => pm.expect(json.count).to.eql(json.data.length));",
|
||||
"json.data.forEach((cache) => {",
|
||||
" pm.test(`캐시 [${cache.name}] 는 keys 배열을 가짐`, () => pm.expect(cache.keys).to.be.an('array'));",
|
||||
"});"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,353 +0,0 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "d4a8b2f0-5e3c-4d21-9f0a-tools-manage-0001",
|
||||
"name": "ToolsManage API (/manage/tools)",
|
||||
"description": "base64/hex 변환, DAMO 및 EncryptionManager 암복호화 테스트, 배포 버전 확인용 유틸리티 API 샘플 모음.\n\n사용 전 확인사항:\n1. Collection Variables 의 baseUrl 을 실제 서버 주소로 변경\n2. DAMO/EncryptionManager 암복호화는 각 실행 순서(암호화 → 복호화)를 지켜야 체이닝 변수가 채워짐\n3. EncryptionManager 는 encryptYN=N 환경에서는 encrypt/decrypt가 원문을 그대로 반환한다(정상 동작). 먼저 '상태 조회'로 확인할 것\n4. /manage/tools/version 은 eapim-online(WAR 루트) build.gradle의 generateVersionInfo 태스크가 생성한 version.info 리소스가 있어야 success=true로 응답한다. 재빌드 없이 IDE에서 바로 기동한 로컬 환경 등에서는 success=false + message로 응답하며, 이는 정상 동작이다.",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"variable": [
|
||||
{ "key": "baseUrl", "value": "http://localhost:8080", "type": "string" },
|
||||
{ "key": "charset", "value": "UTF-8", "type": "string" },
|
||||
{ "key": "plainText", "value": "테스트 평문입니다", "type": "string" },
|
||||
{ "key": "base64EncodedText", "value": "", "type": "string" },
|
||||
{ "key": "hexEncodedText", "value": "", "type": "string" },
|
||||
{ "key": "damoCipherText", "value": "", "type": "string" },
|
||||
{ "key": "encManagerCipherText", "value": "", "type": "string" }
|
||||
],
|
||||
"item": [
|
||||
{
|
||||
"name": "1. Base64 / Hex 변환",
|
||||
"item": [
|
||||
{
|
||||
"name": "문자열 → Base64 인코딩",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"text\": \"{{plainText}}\",\n \"charset\": \"{{charset}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/tools/base64/encode",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "tools", "base64", "encode"]
|
||||
},
|
||||
"description": "charset(생략 시 UTF-8) 기준으로 문자열을 바이트로 변환한 뒤 Base64로 인코딩한다."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('data는 문자열', () => pm.expect(json.data).to.be.a('string'));",
|
||||
"pm.collectionVariables.set('base64EncodedText', json.data);"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Base64 → 문자열 디코딩 (위 인코딩 결과 사용)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"text\": \"{{base64EncodedText}}\",\n \"charset\": \"{{charset}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/tools/base64/decode",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "tools", "base64", "decode"]
|
||||
},
|
||||
"description": "바로 위 '문자열 → Base64 인코딩' 요청에서 저장한 base64EncodedText를 디코딩하여 원문과 일치하는지 확인한다. 반드시 인코딩 요청을 먼저 실행할 것."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('원문과 일치 (라운드트립)', () => pm.expect(json.data).to.eql(pm.collectionVariables.get('plainText')));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "문자열 → Hex 인코딩",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"text\": \"{{plainText}}\",\n \"charset\": \"{{charset}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/tools/hex/encode",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "tools", "hex", "encode"]
|
||||
},
|
||||
"description": "charset(생략 시 UTF-8) 기준으로 문자열을 바이트로 변환한 뒤 대문자 Hex 문자열로 인코딩한다."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('data는 문자열', () => pm.expect(json.data).to.be.a('string'));",
|
||||
"pm.collectionVariables.set('hexEncodedText', json.data);"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Hex → 문자열 디코딩 (위 인코딩 결과 사용)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"text\": \"{{hexEncodedText}}\",\n \"charset\": \"{{charset}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/tools/hex/decode",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "tools", "hex", "decode"]
|
||||
},
|
||||
"description": "바로 위 '문자열 → Hex 인코딩' 요청에서 저장한 hexEncodedText를 디코딩하여 원문과 일치하는지 확인한다. 반드시 인코딩 요청을 먼저 실행할 것."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('원문과 일치 (라운드트립)', () => pm.expect(json.data).to.eql(pm.collectionVariables.get('plainText')));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "2. DAMO 암복호화 (DamoManager 직접 호출)",
|
||||
"item": [
|
||||
{
|
||||
"name": "암호화 (encrypt)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"text\": \"{{plainText}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/tools/damo/encrypt",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "tools", "damo", "encrypt"]
|
||||
},
|
||||
"description": "com.eactive.ext.djb.DamoManager를 직접 호출한다. EncryptionManager의 encryptYN 설정과 무관하게 항상 실제 암호화가 수행된다."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('data는 문자열', () => pm.expect(json.data).to.be.a('string'));",
|
||||
"pm.test('평문과 달라야 함', () => pm.expect(json.data).to.not.eql(pm.collectionVariables.get('plainText')));",
|
||||
"pm.collectionVariables.set('damoCipherText', json.data);"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "복호화 (decrypt) — 위 암호화 결과 사용",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"text\": \"{{damoCipherText}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/tools/damo/decrypt",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "tools", "damo", "decrypt"]
|
||||
},
|
||||
"description": "바로 위 '암호화 (encrypt)' 요청에서 저장한 damoCipherText를 복호화하여 원문과 일치하는지 확인한다. 반드시 암호화 요청을 먼저 실행할 것."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('원문과 일치 (라운드트립)', () => pm.expect(json.data).to.eql(pm.collectionVariables.get('plainText')));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "3. EncryptionManager 암복호화 (운영 설정 반영)",
|
||||
"item": [
|
||||
{
|
||||
"name": "현재 설정 상태 조회",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/tools/encryption-manager/status",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "tools", "encryption-manager", "status"]
|
||||
},
|
||||
"description": "encryptYN/dbEncryptSolutionName/encryptEnabled를 조회한다. encryptYN=N이면 아래 encrypt/decrypt가 원문을 그대로 반환하는 것이 정상 동작이니 먼저 이 값을 확인할 것."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('encryptYN 존재', () => pm.expect(json.data.encryptYN).to.exist);",
|
||||
"pm.test('encryptEnabled는 boolean', () => pm.expect(json.data.encryptEnabled).to.be.a('boolean'));",
|
||||
"console.log('EncryptionManager 상태:', JSON.stringify(json.data));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "암호화 (encrypt)",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"text\": \"{{plainText}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/tools/encryption-manager/encrypt",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "tools", "encryption-manager", "encrypt"]
|
||||
},
|
||||
"description": "실제 운영 코드가 사용하는 EncryptionManager.encryptDBData를 그대로 호출한다. encryptYN=N 환경에서는 원문이 그대로 반환된다(정상 동작)."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('data는 문자열', () => pm.expect(json.data).to.be.a('string'));",
|
||||
"pm.collectionVariables.set('encManagerCipherText', json.data);"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "복호화 (decrypt) — 위 암호화 결과 사용",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [{ "key": "Content-Type", "value": "application/json" }],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"text\": \"{{encManagerCipherText}}\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/tools/encryption-manager/decrypt",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "tools", "encryption-manager", "decrypt"]
|
||||
},
|
||||
"description": "바로 위 '암호화 (encrypt)' 요청에서 저장한 encManagerCipherText를 복호화한다. encryptYN 설정값과 무관하게 encrypt→decrypt 라운드트립 결과는 항상 원문과 같아야 한다(활성 시 실제 복호화, 비활성 시 원문 그대로 통과). 반드시 암호화 요청을 먼저 실행할 것."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"pm.test('success = true', () => pm.expect(json.success).to.eql(true));",
|
||||
"pm.test('원문과 일치 (라운드트립)', () => pm.expect(json.data).to.eql(pm.collectionVariables.get('plainText')));"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "4. 버전 정보",
|
||||
"item": [
|
||||
{
|
||||
"name": "배포 버전 조회",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"header": [],
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/manage/tools/version",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["manage", "tools", "version"]
|
||||
},
|
||||
"description": "eapim-online(WAR를 만드는 루트 프로젝트) build.gradle의 generateVersionInfo 태스크가 생성한 version.info(git describe 결과, buildTime)를 반환한다. 재빌드 없이 IDE에서 바로 기동한 로컬 환경 등 파일이 없으면 success=false + message로 응답하며, 이는 정상 동작이다(서버 500 아님)."
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"type": "text/javascript",
|
||||
"exec": [
|
||||
"const json = pm.response.json();",
|
||||
"pm.test('status 200', () => pm.response.to.have.status(200));",
|
||||
"if (json.success) {",
|
||||
" pm.test('version 존재', () => pm.expect(json.data.version).to.be.a('string'));",
|
||||
" pm.test('buildTime 존재', () => pm.expect(json.data.buildTime).to.be.a('string'));",
|
||||
"} else {",
|
||||
" pm.test('미생성 환경은 message 포함', () => pm.expect(json.message).to.be.a('string'));",
|
||||
" console.log('version.info 없음:', json.message);",
|
||||
"}"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
# comment start with #
|
||||
# level; // 0 : root ~ n
|
||||
# type; // 0: Message, 1: Field, 2:Group 3, Grid, 4 : Field Array, 9 : BIZ DATA
|
||||
# size // 0 : variable, > 0 fixed
|
||||
# fieldType; // 0: ELEMENT, 1: ATTRIBUTE,
|
||||
# length;
|
||||
# dataType; // 1: String, 2: Number,11: ZZ String, 12: LL Number
|
||||
#-----------------------------------------------------------------------------
|
||||
# name ,level ,type ,size ,fieldType ,length ,dataType ,,refPath, refValue,value
|
||||
#-----------------------------------------------------------------------------
|
||||
ElinkHeader ,0,0,1,0, 0,0,,,
|
||||
Header ,1,2,1,1, 0,0,,,
|
||||
StndCicsTrncd ,2,1,1,1,10,0,,,
|
||||
StndIntnlStndTelgmLen,2,1,1,1,10,11,,,0
|
||||
StndTranBaseYmd ,2,1,1,1,10,0,,,
|
||||
Common ,1,2,0,0, 0,0,Header.StndCicsTrncd,JI6H,null
|
||||
TranInfo ,2,2,1,0, 0,0,,,
|
||||
StndCicsTrncd ,3,1,1,0, 3,0,,,
|
||||
StndTelgmRecvTranCd ,3,1,1,0,10,0,,,
|
||||
StndPrcssRtdTranCd ,3,1,1,0,10,0,,,
|
||||
Array ,1,3,0,0, 3,0,,,
|
||||
Item1 ,2,1,1,0,10,0,,,JI6H
|
||||
Group ,2,3,1,0, 1,0,,,
|
||||
gitem1 ,3,1,1,0,10,1,,,726
|
||||
gitem2 ,3,1,1,0,10,0,,,20220905
|
||||
FArray ,1,4,0,0,10,0,,,
|
||||
bizData ,1,9,1,0, 0,0,,,
|
||||
zzData ,1,1,1,0, 2,10,,,ZZ
|
||||
|
@@ -0,0 +1,41 @@
|
||||
#-----------------------------------------------------------------------------
|
||||
# comment start with #
|
||||
# level; // 0 : root ~ n
|
||||
# type; // 0: Message, 1: Field, 2:Group 3, Grid, 4 : Field Array, 9 : BIZ DATA
|
||||
# size // 0 : variable, > 0 fixed
|
||||
# fieldType; // 0: ELEMENT, 1: ATTRIBUTE,
|
||||
# length;
|
||||
# dataType; // 1: String, 2: Number,11: ZZ String, 12: LL Number
|
||||
#-----------------------------------------------------------------------------
|
||||
# name ,level ,type ,size ,fieldType ,length ,dataType ,,refPath, refValue,value
|
||||
#-----------------------------------------------------------------------------
|
||||
# name ,level ,type ,size ,fieldType ,length ,dataType ,refPath, refValue, value
|
||||
STD_HNCPHEADER_REQ ,0 ,1 ,1 ,1 ,0 ,1 , , ,,표준헤더
|
||||
msg_len ,1 ,2 ,1 ,1 ,6 ,12 , , ,199,전문길이
|
||||
global_id ,1 ,2 ,1 ,1 ,29 ,1 , , ,GIDNO00000000001111,Global아이디
|
||||
service_id ,1 ,2 ,1 ,1 ,10 ,1 , , ,ELINKSVC01,거래코드
|
||||
service_cmt ,1 ,2 ,1 ,1 ,100 ,1 , , ,ELINK TEST Service,거래코드설명
|
||||
service_tn ,1 ,2 ,1 ,1 ,1 ,1 , , ,1,거래구분
|
||||
Session_id ,1 ,2 ,1 ,1 ,100 ,1 , , ,SESSIONID001,Session아이디
|
||||
encrypt_yn ,1 ,2 ,1 ,1 ,2 ,1 , , ,NO,전문암호화여부
|
||||
http_url ,1 ,2 ,1 ,1 ,100 ,1 , , ,,HTTPURL정보
|
||||
screen_id ,1 ,2 ,1 ,1 ,13 ,1 , , ,SCREEN0000001,화면ID
|
||||
scr_no ,1 ,2 ,1 ,1 ,30 ,1 , , ,000000000100000000020000000003,화면번호
|
||||
scr_dsc ,1 ,2 ,1 ,1 ,100 ,1 , , ,,화면명
|
||||
usr_id ,1 ,2 ,1 ,1 ,10 ,1 , , ,USER000001,사용자ID
|
||||
ip_addr ,1 ,2 ,1 ,1 ,47 ,1 , , ,,IPAddress
|
||||
ip_mac ,1 ,2 ,1 ,1 ,12 ,1 , , ,,ClientMAC Address
|
||||
com_code ,1 ,2 ,1 ,1 ,4 ,1 , , ,EACT,그룹사코드
|
||||
head_code ,1 ,2 ,1 ,1 ,7 ,1 , , ,EACT001,본부코드
|
||||
dept_cd ,1 ,2 ,1 ,1 ,7 ,1 , , ,ELINK01,부서코드
|
||||
conn_tp ,1 ,2 ,1 ,1 ,2 ,1 , , ,CH,요청채널구분
|
||||
Inter_chn ,1 ,2 ,1 ,1 ,2 ,1 , , ,EA,시스템구분
|
||||
recv_tm ,1 ,2 ,1 ,1 ,17 ,1 , , ,20220928000000000,전문수신시간
|
||||
send_tm ,1 ,2 ,1 ,1 ,17 ,1 , , ,,전문송신시간
|
||||
return_gubun ,1 ,2 ,1 ,1 ,1 ,1 , , ,S,전문응답구분
|
||||
return_value ,1 ,2 ,1 ,1 ,1 ,1 , , ,,처리결과
|
||||
msg_id ,1 ,2 ,1 ,1 ,7 ,1 , , ,,오류코드
|
||||
msg_nm ,1 ,2 ,1 ,1 ,250 ,1 , , ,,오류메시지
|
||||
filler ,1 ,2 ,1 ,1 ,125 ,1 , , ,,Filler
|
||||
bizData ,1 ,9 ,0 ,1 ,0 ,1 , , ,,업무데이터
|
||||
zzData ,1 ,2 ,0 ,1 ,2 ,11 , , ,,ZZ플래그
|
||||
|
@@ -0,0 +1,11 @@
|
||||
layout.file.type=CSV
|
||||
layout.file.path=./resources/standard-layout-sample.csv
|
||||
mapper.class=com.eactive.eai.message.test.TestInterfaceMapper
|
||||
mapper.definition=./resources/standard-message-mapping-config.properties
|
||||
reader.JSON=com.eactive.eai.message.parser.JsonReader
|
||||
reader.JSN=com.eactive.eai.message.parser.JsonReader
|
||||
reader.UJN=com.eactive.eai.message.parser.JsonReader
|
||||
reader.XML=com.eactive.eai.message.parser.XmlReader
|
||||
reader.UXL=com.eactive.eai.message.parser.XmlReader
|
||||
reader.ASC=com.eactive.eai.message.parser.FlatReader
|
||||
reader.FLAT=com.eactive.eai.message.parser.FlatReader
|
||||
@@ -0,0 +1,20 @@
|
||||
# layout : StandardMessage layout definition
|
||||
layout.file.type=CSV
|
||||
layout.file.path=./resources/standard-layout.csv
|
||||
layout.filter.FLAT=com.eactive.eai.message.filter.FlatMessageFilter
|
||||
# mapper : StandardMessage's fields getter/setter interface
|
||||
mapper.class=com.eactive.eai.message.service.DefaultInterfaceMapper
|
||||
mapper.definition=./resources/standard-message-mapping-config.properties
|
||||
# reader : parsing input data to StandardMessage
|
||||
reader.JSON=com.eactive.eai.message.parser.JsonReader
|
||||
reader.JSN=com.eactive.eai.message.parser.JsonReader
|
||||
reader.UJN=com.eactive.eai.message.parser.JsonReader
|
||||
reader.XML=com.eactive.eai.message.parser.XmlReader
|
||||
reader.UXL=com.eactive.eai.message.parser.XmlReader
|
||||
reader.ASC=com.eactive.eai.message.parser.FlatReader
|
||||
reader.FLAT=com.eactive.eai.message.parser.FlatReader
|
||||
encode.flat=euc-kr
|
||||
encode.value=euc-kr
|
||||
encode.xml=utf-8
|
||||
# version.layout.name=VERSION_SAMPLE
|
||||
# version.mapper.definition=./resources/version-message-mapping-config.properties
|
||||
@@ -0,0 +1,28 @@
|
||||
REQ_SYS_CODE=systemHeader.DMND_SYS_CD
|
||||
CHANNEL_TYPE_CD=stdcommon.chnlTycd
|
||||
CHANNEL_DETAIL_CD=stdcommon.chnlDtlsClcd
|
||||
SEND_TIME=stdheader.send_tm
|
||||
GUID=stdheader.global_id
|
||||
SYSTEM_TYPE=stdheader.Inter_chn
|
||||
RECV_TIME=stdheader.recv_tm
|
||||
RETURN_VALUE=stdheader.return_value
|
||||
GUID_ORG=stdcommon.ortrGuid
|
||||
GUID_SEQ=stdheader.global_id
|
||||
#SERVICE_ID=stdheader.service_id
|
||||
SERVICE_ID=service_id
|
||||
RETURN_SERVICE_ID=stdcommon.procsRsltRcmsSrvcId
|
||||
INTERFACE_ID=stdheader.service_id
|
||||
SESSION_ID=stdheader.Session_id
|
||||
#SEND_RECV_DIVISION=stdheader.return_gubun
|
||||
SEND_RECV_DIVISION=service_tn
|
||||
IN_EX_DIVISION=stdcommon.hmabDvcd
|
||||
OPERATION_ENV=stdcommon.sysOprtEnvDvcd
|
||||
FIRST_REQ_IP=stdheader.ip_addr
|
||||
RECOVER_YN=stdcommon.ortrRestrYn
|
||||
INST_CODE=stdheader.com_code
|
||||
TIMEOUT=stdheader.gnrzTmotTktm
|
||||
ERROR_CODE=stdheader.msg_id
|
||||
ERROR_MSG=stdheader.msg_nm
|
||||
USER_ID=stdheader.usr_id
|
||||
RESPONSE_TYPE=stdcommon.procsRsltDvcd
|
||||
SYNC_ASYNC_TYPE=stdcommon.txSynzDvcd
|
||||
@@ -0,0 +1,26 @@
|
||||
REQ_SYS_CODE=Inter_chn
|
||||
CHANNEL_TYPE_CD=
|
||||
CHANNEL_DETAIL_CD=conn_tp
|
||||
SEND_TIME=send_tm
|
||||
GUID=global_id
|
||||
SYSTEM_TYPE=service_tn
|
||||
RECV_TIME=recv_tm
|
||||
RETURN_VALUE=return_value
|
||||
GUID_ORG=stdcommon.ortrGuid
|
||||
GUID_SEQ=stdheader.global_id
|
||||
SERVICE_ID=service_id
|
||||
RETURN_SERVICE_ID=stdcommon.procsRsltRcmsSrvcId
|
||||
INTERFACE_ID=service_id
|
||||
SESSION_ID=Session_id
|
||||
SEND_RECV_DIVISION=service_tn
|
||||
IN_EX_DIVISION=stdcommon.hmabDvcd
|
||||
OPERATION_ENV=stdcommon.sysOprtEnvDvcd
|
||||
FIRST_REQ_IP=ip_addr
|
||||
RECOVER_YN=stdcommon.ortrRestrYn
|
||||
INST_CODE=stdheader.com_code
|
||||
TIMEOUT=stdheader.gnrzTmotTktm
|
||||
ERROR_CODE=msg_id
|
||||
ERROR_MSG=msg_nm
|
||||
USER_ID=usr_id
|
||||
RESPONSE_TYPE=stdcommon.procsRsltDvcd
|
||||
SYNC_ASYNC_TYPE=stdcommon.txSynzDvcd
|
||||
@@ -0,0 +1,4 @@
|
||||
service_id=header.if_id
|
||||
global_id=header.guid
|
||||
service_tn=header.req_type
|
||||
bizData=bizData
|
||||
@@ -6,7 +6,6 @@ import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -164,9 +163,6 @@ public class AdapterManager implements Lifecycle {
|
||||
}
|
||||
|
||||
public AdapterVO getAdapterVO(String adapterGroupName, String adapterName) {
|
||||
if(StringUtils.isAnyBlank(adapterGroupName, adapterName))
|
||||
return null;
|
||||
|
||||
AdapterGroupVO gvo = adapterGroups.get(adapterGroupName);
|
||||
if (gvo == null)
|
||||
return null;
|
||||
|
||||
@@ -2,50 +2,9 @@ package com.eactive.eai.adapter.handler;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import com.eactive.eai.common.message.EAIMessage;
|
||||
import com.eactive.eai.message.StandardMessage;
|
||||
|
||||
public interface AdapterErrorMessageHandler {
|
||||
/**
|
||||
* 비표준 -> 표준 거래
|
||||
* 아웃바운드 DefaultProcess에서 응답 표준헤더에 에러응답이 세팅되어 있는 경우, 비표준 응답 메시지 생성용
|
||||
*/
|
||||
public Object generateNonStandardErrorResponseMessage(String inboudnAdapterGroupName, String inboudnAdapterName,
|
||||
Properties callProp, Object outboundRequestData, EAIMessage resEaiMsg) throws Exception;
|
||||
|
||||
/**
|
||||
* 비표준 -> 표준 거래
|
||||
* GW 내부 오류가 발생한 경우, RequestPrcessor에서 에러 응답을 생성하는 경우, ExceptionHandler에 의해 호출, 비표준 응답 메시지 생성용
|
||||
*/
|
||||
public Object generateNonStandardInternalErrorResponseMessage(String inboudnAdapterGroupName,
|
||||
String inboudnAdapterName, Properties callProp, Object inboundRequestData, EAIMessage resEaiMsg)
|
||||
throws Exception;
|
||||
|
||||
/**
|
||||
* 비표준 -> 표준 거래
|
||||
* GW RequestProcess 호출 전에 Adapter(Filter)에서 오류가 발생한 경우, ApiAdapterController에 Exception에 맞추어, 비표준 응답 메시지 생성용
|
||||
*/
|
||||
public Object generateNonStandardInboundErrorResponseMessage(String inboudnAdapterGroupName,
|
||||
String inboudnAdapterName, Properties callProp, Object inboundRequestData, Object inboundResponseData,
|
||||
Throwable e) throws Exception;
|
||||
|
||||
/**
|
||||
* 표준 -> 비표준 거래
|
||||
* 아웃바운드 어댑터에서 Exception이 발생했을 때, Outbound 어댑터에 의해서 호출하여 응답 메시지 생성
|
||||
* 수신한 응답 메시지가 있을 수도 있고 없을 수도 있다.
|
||||
*/
|
||||
default public Object generateOutboundErrorResponseMessage(String outboundadapterGroupName,
|
||||
String outboundadapterName, Properties callProp, Object outboundRequestData, Object outboundResponseData,
|
||||
Throwable e) throws Exception {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 표준 -> 비표준 거래
|
||||
* 아웃바운드 어댑터에서 Exception이 발생했을 때, Outbound Process 응답 메시지 생성
|
||||
*/
|
||||
default public Object generateOutboundErrorResponseMessage(String outboundadapterGroupName,
|
||||
String outboundadapterName, Properties callProp, Object outboundRequestData, EAIMessage resEaiMsg)
|
||||
throws Exception {
|
||||
return null;
|
||||
}
|
||||
public Object generateNonStandardErrorResponseMessage(String inboudnAdapterGroupName, String inboudnAdapterName, Properties callProp, Object outboundRequestData, StandardMessage resStandardMessage) throws Exception;
|
||||
}
|
||||
|
||||
+2
-14
@@ -2,25 +2,13 @@ package com.eactive.eai.adapter.handler;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import com.eactive.eai.common.message.EAIMessage;
|
||||
import com.eactive.eai.message.StandardMessage;
|
||||
|
||||
public class DefaultAdapterErrorMessageHandler implements AdapterErrorMessageHandler {
|
||||
|
||||
@Override
|
||||
public Object generateNonStandardErrorResponseMessage(String inboudnAdapterGroupName, String inboudnAdapterName,
|
||||
Properties callProp, Object outboundRequestData, EAIMessage resEaiMsg) throws Exception {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object generateNonStandardInternalErrorResponseMessage(String inboudnAdapterGroupName, String inboudnAdapterName,
|
||||
Properties callProp, Object inboundRequestData, EAIMessage resEaiMsg) throws Exception {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object generateNonStandardInboundErrorResponseMessage(String adapterGroupName, String adapterName,
|
||||
Properties callProp, Object inboundRequestData, Object inboundResponseData, Throwable e) throws Exception {
|
||||
Properties callProp, Object outboundRequestData, StandardMessage resStandardMessage) throws Exception {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,569 +0,0 @@
|
||||
package com.eactive.eai.adapter.handler;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import com.eactive.eai.adapter.AdapterGroupVO;
|
||||
import com.eactive.eai.adapter.AdapterManager;
|
||||
import com.eactive.eai.adapter.AdapterPropManager;
|
||||
import com.eactive.eai.adapter.AdapterVO;
|
||||
import com.eactive.eai.adapter.http.HttpStatusException;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.FilterCryptoException;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
|
||||
import com.eactive.eai.common.message.EAIMessage;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.common.util.MessageUtil;
|
||||
import com.eactive.eai.common.util.MessageUtil;
|
||||
import com.eactive.eai.message.StandardItem;
|
||||
import com.eactive.eai.message.StandardMessage;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
/**
|
||||
* 템플릿 기반 어댑터 에러 메시지 핸들러.
|
||||
*
|
||||
* [프로퍼티 설정]
|
||||
* AdapterPropManager 의 "AdapterErrorMessageHandler" 프로퍼티 그룹에서
|
||||
* "{adapterGroupId}.template" 키로 등록된 템플릿 문자열을 읽어
|
||||
* StandardMessage 의 필드 값으로 치환한 결과를 반환한다.
|
||||
*
|
||||
* [변수 치환 규칙]
|
||||
* 1. "${callprop.키}" → callProp.getProperty("키") 로 치환
|
||||
* "${callprop.키:기본값}" → 키 없거나 callProp null 이면 기본값 반환
|
||||
* 2. "${callprop[경로]}" → StandardMessage 경로 값을 키로 callProp 간접 조회
|
||||
* "${callprop[경로]:기본값}" → 키 결정 실패 또는 키 없으면 기본값 반환
|
||||
* 3. "${경로}" → StandardMessage.findItemValue("경로") 로 치환
|
||||
* "${경로:기본값}" → 경로 값이 null/공백이면 기본값 반환
|
||||
* 모든 형태에서 기본값 미지정 시 빈 문자열을 반환한다.
|
||||
*
|
||||
* [배열 반복 문법] (MSG_LIST 등 GRID 타입)
|
||||
* {{#foreach MSG.MSG_LIST}}
|
||||
* { "code":"${outp_msg_cd}", "msg":"${outp_msg_ctnt}" }
|
||||
* {{/foreach}}
|
||||
* → MSG_LIST 각 행을 반복하며 행 내 필드명(접두어 없이)으로 치환,
|
||||
* 반복 결과를 쉼표(,)로 이어 붙임
|
||||
*
|
||||
* [사용 예 – JSON 템플릿]
|
||||
* {
|
||||
* "adapterName": "${callprop.ADAPTER_NAME}",
|
||||
* "resultCode": "${MSG.MAIN_MSG.outp_msg_cd}",
|
||||
* "resultMsg": "${MSG.MAIN_MSG.outp_msg_ctnt}",
|
||||
* "errors": [
|
||||
* {{#foreach MSG.MSG_LIST}}
|
||||
* {
|
||||
* "code": "${outp_msg_cd}",
|
||||
* "msg": "${outp_msg_ctnt}",
|
||||
* "desc": "${outp_msg_desc}",
|
||||
* "field": "${err_occu_item_nm}"
|
||||
* }
|
||||
* {{/foreach}}
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* [사용 예 – XML 템플릿]
|
||||
* <error>
|
||||
* <adapter>${callprop.ADAPTER_NAME}</adapter>
|
||||
* <code>${MSG.MAIN_MSG.outp_msg_cd}</code>
|
||||
* <errors>
|
||||
* {{#foreach MSG.MSG_LIST}}
|
||||
* <item><code>${outp_msg_cd}</code><msg>${outp_msg_ctnt}</msg></item>
|
||||
* {{/foreach}}
|
||||
* </errors>
|
||||
* </error>
|
||||
*
|
||||
* [callProp 중첩 Properties 참조]
|
||||
* callProp 의 값이 Properties 인 경우 점(.) 으로 체인 탐색이 가능하다.
|
||||
* callProp.put("OUTBOUND", subProps) // subProps.setProperty("IF_ID", "SVC001")
|
||||
* → 템플릿에서 ${callprop.OUTBOUND.IF_ID} 로 참조
|
||||
* 중간 값이 Properties 가 아닌 경우 전체 keyPath 를 키로 폴백 조회한다.
|
||||
*/
|
||||
public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandler {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
/** PropManager 에서 템플릿을 조회할 프로퍼티 그룹 이름 */
|
||||
static final String PROP_GROUP = "AdapterErrorMessageHandler";
|
||||
|
||||
/** callProp 참조 변수 접두어: ${callprop.키} */
|
||||
static final String CALLPROP_PREFIX = "callprop.";
|
||||
|
||||
/** callProp 간접 참조 접두어: ${callprop[표준전문경로]} */
|
||||
static final String CALLPROP_INDIRECT_PREFIX = "callprop[";
|
||||
|
||||
/** 예외 객체 필드 참조 접두어: ${exception.필드명} */
|
||||
static final String EXCEPTION_PREFIX = "exception.";
|
||||
|
||||
/** ${path} 또는 ${callprop.key} 스칼라 변수 패턴 (renderRow 내부에서 사용) */
|
||||
private static final Pattern VAR_PATTERN =
|
||||
Pattern.compile("\\$\\{([^}]+)\\}");
|
||||
|
||||
/**
|
||||
* foreach 블록과 스칼라 변수를 순서대로 단일 패스로 처리하는 통합 패턴.
|
||||
* group(1) != null → foreach 블록: group(1)=경로, group(2)=블록 본문
|
||||
* group(3) != null → 스칼라 변수: group(3)=표현식
|
||||
*/
|
||||
private static final Pattern COMBINED_PATTERN =
|
||||
Pattern.compile(
|
||||
"\\{\\{#foreach\\s+([^}]+)\\}\\}(.*?)\\{\\{/foreach\\}\\}|\\$\\{([^}]+)\\}",
|
||||
Pattern.DOTALL);
|
||||
|
||||
@Override
|
||||
public Object generateNonStandardErrorResponseMessage(
|
||||
String inboundAdapterGroupName,
|
||||
String inboundAdapterName,
|
||||
Properties callProp,
|
||||
Object outboundRequestData,
|
||||
EAIMessage resEaiMsg) throws Exception {
|
||||
|
||||
StandardMessage resStandardMessage = resEaiMsg.getStandardMessage();
|
||||
String templateKey = inboundAdapterGroupName + ".template";
|
||||
String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
|
||||
|
||||
if (StringUtils.isBlank(template))
|
||||
template = PropManager.getInstance().getProperty(PROP_GROUP, "default.template");
|
||||
|
||||
if (StringUtils.isBlank(template)) {
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("TemplateAdapterErrorMsgHandler] template not found. group=" + PROP_GROUP
|
||||
+ ", key=" + templateKey);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return render(template, resStandardMessage, callProp);
|
||||
}
|
||||
|
||||
/** 3-인자 위임: exception 없이 호출하는 기존 경로 유지 */
|
||||
String render(String template, StandardMessage msg, Properties callProp) {
|
||||
return render(template, msg, callProp, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿에 StandardMessage, callProp, exception 값을 치환해 최종 문자열을 반환한다.
|
||||
* foreach 블록과 스칼라 변수를 좌→우 순서로 단일 패스 처리한다.
|
||||
* package-private: 단위 테스트에서 직접 호출 가능
|
||||
*/
|
||||
String render(String template, StandardMessage msg, Properties callProp, Throwable exception) {
|
||||
// 관리 UI 등에서 HTML 인코딩된 엔티티를 복원 (예: # → #)
|
||||
template = template.replace("#", "#")
|
||||
.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace(""", "\"");
|
||||
StringBuffer result = new StringBuffer();
|
||||
Matcher matcher = COMBINED_PATTERN.matcher(template);
|
||||
while (matcher.find()) {
|
||||
String replacement;
|
||||
if (matcher.group(1) != null) {
|
||||
// {{#foreach path}}...{{/foreach}} 블록
|
||||
String arrayPath = matcher.group(1).trim();
|
||||
String blockContent = matcher.group(2);
|
||||
replacement = (msg != null) ? renderForeach(arrayPath, blockContent, msg) : "";
|
||||
} else {
|
||||
// ${path} / ${callprop.키} / ${exception.필드} 스칼라 변수
|
||||
String expr = matcher.group(3).trim();
|
||||
// 치환값에 제어문자가 섞이면 렌더 결과가 깨진 JSON/XML 이 된다.
|
||||
// (개행이 든 값 → {"outpMsgDesc":"오류상세<개행>..."} → 수신측 파싱 실패)
|
||||
// 템플릿 포맷을 알 수 없으므로 이스케이프 대신 제어문자를 걸러낸다.
|
||||
replacement = MessageUtil.stripControlChars(resolveScalar(expr, msg, callProp, exception));
|
||||
}
|
||||
matcher.appendReplacement(result, Matcher.quoteReplacement(replacement));
|
||||
}
|
||||
matcher.appendTail(result);
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 변수 표현식을 해석해 값을 반환한다.
|
||||
* 모든 형태에서 ':기본값' 접미사를 지원한다. 기본값 미지정 시 빈 문자열.
|
||||
* - "callprop[경로][:기본값]" → 표준전문 경로 값을 키로 callProp 간접 조회
|
||||
* - "callprop.키[:기본값]" → callProp 직접/중첩 조회
|
||||
* - "exception.필드[:기본값]" → 리플렉션으로 예외 객체 getter/필드 조회
|
||||
* - "경로[:기본값]" → StandardMessage.findItemValue(경로)
|
||||
*/
|
||||
private String resolveScalar(String expr, StandardMessage msg, Properties callProp, Throwable exception) {
|
||||
if (expr.startsWith(CALLPROP_INDIRECT_PREFIX)) {
|
||||
return resolveIndirectCallProp(expr, msg, callProp);
|
||||
}
|
||||
if (expr.startsWith(CALLPROP_PREFIX)) {
|
||||
String rest = expr.substring(CALLPROP_PREFIX.length());
|
||||
int colonIdx = rest.indexOf(':');
|
||||
String keyPath = colonIdx >= 0 ? rest.substring(0, colonIdx) : rest;
|
||||
String defaultVal = colonIdx >= 0 ? rest.substring(colonIdx + 1) : "";
|
||||
if (callProp == null) return defaultVal;
|
||||
String value = resolveCallProp(callProp, keyPath);
|
||||
return value != null ? value : defaultVal;
|
||||
}
|
||||
if (expr.startsWith(EXCEPTION_PREFIX)) {
|
||||
String rest = expr.substring(EXCEPTION_PREFIX.length());
|
||||
int colonIdx = rest.indexOf(':');
|
||||
String fieldName = colonIdx >= 0 ? rest.substring(0, colonIdx) : rest;
|
||||
String defaultVal = colonIdx >= 0 ? rest.substring(colonIdx + 1) : "";
|
||||
if (exception == null) return defaultVal;
|
||||
String value = resolveExceptionProperty(exception, fieldName);
|
||||
return value != null ? value : defaultVal;
|
||||
}
|
||||
int colonIdx = expr.indexOf(':');
|
||||
String path = colonIdx >= 0 ? expr.substring(0, colonIdx) : expr;
|
||||
String defaultVal = colonIdx >= 0 ? expr.substring(colonIdx + 1) : "";
|
||||
String value = resolveMessagePath(msg, path);
|
||||
return StringUtils.isNotEmpty(value) ? value : defaultVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* StandardMessage 경로를 해석한다.
|
||||
* 1. 경로에 '[n]' 구문이 있으면 배열 인덱스 접근으로 처리한다.
|
||||
* 예) MSG.MSG_LIST[0].outp_msg_cd → MSG_LIST 첫 번째 행의 outp_msg_cd 값
|
||||
* 2. findItemValue(path) 로 직접 조회한다.
|
||||
* 3. null 이면 경로를 뒤에서부터 분리하며, 부모 경로 값이 JSON 문자열인 경우
|
||||
* 나머지 경로(subPath)를 키로 JSON 필드를 추출한다.
|
||||
* 예) DATA.BIZDATA.acctNo → findItemValue("DATA.BIZDATA") → JSON 파싱 → acctNo
|
||||
* DATA.BIZDATA.addr.city → findItemValue("DATA.BIZDATA") → JSON 파싱 → addr.city
|
||||
*/
|
||||
private String resolveMessagePath(StandardMessage msg, String path) {
|
||||
if (msg == null) return null;
|
||||
|
||||
int bracketStart = path.indexOf('[');
|
||||
if (bracketStart >= 0) {
|
||||
return resolveArrayIndex(msg, path, bracketStart);
|
||||
}
|
||||
|
||||
String value = msg.findItemValue(path);
|
||||
if (value != null) return value;
|
||||
|
||||
int dotIdx = path.lastIndexOf('.');
|
||||
while (dotIdx > 0) {
|
||||
String parentPath = path.substring(0, dotIdx);
|
||||
String subPath = path.substring(dotIdx + 1);
|
||||
String parentVal = msg.findItemValue(parentPath);
|
||||
if (StringUtils.isNotBlank(parentVal)) {
|
||||
String extracted = extractFromJson(parentVal, subPath);
|
||||
if (extracted != null) return extracted;
|
||||
}
|
||||
dotIdx = parentPath.lastIndexOf('.');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 배열 인덱스 접근 구문 처리: {arrayPath}[{n}].{fieldName}
|
||||
* - arrayPath : GRID 타입 아이템 경로 (예: MSG.MSG_LIST)
|
||||
* - n : 0-based 행 인덱스
|
||||
* - fieldName : 행 내 컬럼명 (예: outp_msg_cd)
|
||||
* 인덱스가 범위를 벗어나거나 필드가 없으면 null 반환.
|
||||
*/
|
||||
private String resolveArrayIndex(StandardMessage msg, String path, int bracketStart) {
|
||||
if (msg == null) return null;
|
||||
|
||||
int bracketEnd = path.indexOf(']', bracketStart);
|
||||
if (bracketEnd < 0) return null;
|
||||
|
||||
String arrayPath = path.substring(0, bracketStart);
|
||||
String indexStr = path.substring(bracketStart + 1, bracketEnd);
|
||||
int index;
|
||||
try {
|
||||
index = Integer.parseInt(indexStr.trim());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String fieldName = null;
|
||||
if (bracketEnd + 2 <= path.length() - 1 && path.charAt(bracketEnd + 1) == '.') {
|
||||
fieldName = path.substring(bracketEnd + 2);
|
||||
}
|
||||
if (StringUtils.isBlank(fieldName)) return null;
|
||||
|
||||
StandardItem arrayItem = msg.findItem(arrayPath);
|
||||
if (arrayItem == null) return null;
|
||||
|
||||
List<LinkedHashMap<String, StandardItem>> rows = arrayItem.getList();
|
||||
if (rows == null || index < 0 || index >= rows.size()) return null;
|
||||
|
||||
StandardItem item = rows.get(index).get(fieldName);
|
||||
return item != null ? StringUtils.defaultString(item.getValue()) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 문자열에서 dot 구분 경로로 필드 값을 추출한다.
|
||||
* 중간 경로가 JSON 오브젝트면 계속 탐색하고, 최종 값이 primitive 면 문자열로,
|
||||
* 오브젝트/배열이면 JSON 문자열 그대로 반환한다. 파싱 실패 시 null 반환.
|
||||
*/
|
||||
private String extractFromJson(String json, String fieldPath) {
|
||||
try {
|
||||
JsonElement el = new JsonParser().parse(json);
|
||||
for (String part : fieldPath.split("\\.", -1)) {
|
||||
if (!el.isJsonObject()) return null;
|
||||
el = el.getAsJsonObject().get(part);
|
||||
if (el == null) return null;
|
||||
}
|
||||
return el.isJsonPrimitive() ? el.getAsString() : el.toString();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ${callprop[경로]} 또는 ${callprop[경로]:기본값} 처리.
|
||||
* 1. 표준전문 경로로 callProp 키를 동적으로 결정한다.
|
||||
* 2. 결정된 키로 callProp 에서 값을 조회한다.
|
||||
* 3. 키 또는 값을 찾지 못하면 기본값(없으면 빈 문자열)을 반환한다.
|
||||
*/
|
||||
private String resolveIndirectCallProp(String expr, StandardMessage msg, Properties callProp) {
|
||||
int closeIdx = expr.indexOf(']');
|
||||
if (closeIdx < 0) return "";
|
||||
|
||||
String msgPath = expr.substring(CALLPROP_INDIRECT_PREFIX.length(), closeIdx);
|
||||
String defaultVal = (closeIdx + 1 < expr.length() && expr.charAt(closeIdx + 1) == ':')
|
||||
? expr.substring(closeIdx + 2)
|
||||
: "";
|
||||
|
||||
if (msg == null) return defaultVal;
|
||||
|
||||
String key = msg.findItemValue(msgPath);
|
||||
if (StringUtils.isBlank(key)) return defaultVal;
|
||||
if (callProp == null) return defaultVal;
|
||||
|
||||
String value = resolveCallProp(callProp, key);
|
||||
return value != null ? value : defaultVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties 체인을 점(.) 구분자로 재귀 탐색한다.
|
||||
* - 첫 세그먼트의 값이 Properties 이면 나머지 경로로 재귀
|
||||
* - String 이면 반환, 그 외 Object 이면 toString() 반환
|
||||
* - 첫 세그먼트 값이 Properties 가 아닌 경우 전체 keyPath 로 폴백 조회
|
||||
*/
|
||||
private String resolveCallProp(Properties props, String keyPath) {
|
||||
int dotIdx = keyPath.indexOf('.');
|
||||
if (dotIdx < 0) {
|
||||
Object val = props.get(keyPath);
|
||||
if (val == null) return null;
|
||||
if (val instanceof String) return (String) val;
|
||||
return val.toString();
|
||||
}
|
||||
|
||||
String first = keyPath.substring(0, dotIdx);
|
||||
String rest = keyPath.substring(dotIdx + 1);
|
||||
Object val = props.get(first);
|
||||
if (val instanceof Properties) {
|
||||
return resolveCallProp((Properties) val, rest);
|
||||
}
|
||||
|
||||
// 중간 값이 Properties 가 아닌 경우: 전체 keyPath 를 키로 폴백
|
||||
Object direct = props.get(keyPath);
|
||||
if (direct == null) return null;
|
||||
if (direct instanceof String) return (String) direct;
|
||||
return direct.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* GRID 타입 배열 아이템을 반복하며 blockContent 를 각 행으로 치환,
|
||||
* 결과를 쉼표로 이어 반환한다. (foreach 블록 내부는 callProp 참조 미지원)
|
||||
*/
|
||||
private String renderForeach(String arrayPath, String blockContent, StandardMessage msg) {
|
||||
StandardItem arrayItem = msg.findItem(arrayPath);
|
||||
if (arrayItem == null) {
|
||||
if (logger.isWarn()) logger.warn("TemplateAdapterErrorMsgHandler] foreach path not found: " + arrayPath);
|
||||
return "";
|
||||
}
|
||||
|
||||
List<LinkedHashMap<String, StandardItem>> rows = arrayItem.getList();
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
List<String> renderedItems = new ArrayList<>();
|
||||
for (LinkedHashMap<String, StandardItem> row : rows) {
|
||||
String rowText = renderRow(blockContent, row);
|
||||
if (StringUtils.isNotBlank(rowText)) {
|
||||
renderedItems.add(rowText);
|
||||
}
|
||||
}
|
||||
return String.join(",", renderedItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* 배열 한 행(LinkedHashMap) 기준으로 blockContent 의 ${fieldName} 을 치환한다.
|
||||
* fieldName 은 경로 없이 해당 행의 컬럼명만 사용한다.
|
||||
*/
|
||||
private String renderRow(String blockContent, LinkedHashMap<String, StandardItem> row) {
|
||||
StringBuffer result = new StringBuffer();
|
||||
Matcher varMatcher = VAR_PATTERN.matcher(blockContent);
|
||||
while (varMatcher.find()) {
|
||||
String fieldName = varMatcher.group(1).trim();
|
||||
String value = "";
|
||||
StandardItem item = row.get(fieldName);
|
||||
if (item != null) {
|
||||
// render() 의 스칼라 치환과 동일한 이유로 제어문자를 걸러낸다
|
||||
value = MessageUtil.stripControlChars(StringUtils.defaultString(item.getValue()));
|
||||
}
|
||||
varMatcher.appendReplacement(result, Matcher.quoteReplacement(value));
|
||||
}
|
||||
varMatcher.appendTail(result);
|
||||
return result.toString().trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object generateNonStandardInternalErrorResponseMessage(String inboudnAdapterGroupName, String inboudnAdapterName,
|
||||
Properties callProp, Object inboundRequestData, EAIMessage resEaiMsg) throws Exception {
|
||||
String template = "";
|
||||
String templateKey = "";
|
||||
if (resEaiMsg != null && StringUtils.isNotEmpty(resEaiMsg.getRspErrCd())) {
|
||||
templateKey = inboudnAdapterGroupName + ".sys." + resEaiMsg.getRspErrCd() + ".template";
|
||||
template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
|
||||
}
|
||||
if (StringUtils.isBlank(template)) {
|
||||
templateKey = inboudnAdapterGroupName + ".sys.template";
|
||||
template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
|
||||
}
|
||||
if (StringUtils.isBlank(template))
|
||||
template = PropManager.getInstance().getProperty(PROP_GROUP, "default.sys.template");
|
||||
|
||||
if (StringUtils.isBlank(template)) {
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("TemplateAdapterErrorMsgHandler] template not found. group=" + PROP_GROUP
|
||||
+ ", key=" + templateKey);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return render(template, resEaiMsg.getStandardMessage(), callProp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object generateNonStandardInboundErrorResponseMessage(String adapterGroupName, String adapterName,
|
||||
Properties callProp, Object inboundRequestData, Object inboundResponseData, Throwable e) throws Exception {
|
||||
|
||||
AdapterGroupVO adapterGroupVO = AdapterManager.getInstance().getAdapterGroupVO(adapterGroupName);
|
||||
AdapterVO adapterVO = AdapterManager.getInstance().getAdapterVO(adapterGroupName, adapterName);
|
||||
Properties httpProp = AdapterPropManager.getInstance().getProperties(adapterVO.getPropGroupName());
|
||||
String adptMsgType = adapterVO.getAdapterGroupVO().getMessageType();
|
||||
String encode = StringUtils.defaultIfBlank(adapterGroupVO.getMessageEncode(), "UTF-8");
|
||||
|
||||
if (e instanceof FilterCryptoException) {
|
||||
return genInboundErrorResponse(adapterGroupName, callProp, httpProp, adptMsgType, encode, e, 550);
|
||||
} else if (e instanceof HttpStatusException) {
|
||||
HttpStatusException e1 = (HttpStatusException) e;
|
||||
int httpCode = e1.getStatus();
|
||||
|
||||
return genInboundErrorResponse(adapterGroupName, callProp, httpProp, adptMsgType, encode, e1, httpCode);
|
||||
} else if (e instanceof JwtAuthException) {
|
||||
return genInboundErrorResponse(adapterGroupName, callProp, httpProp, adptMsgType, encode, e,
|
||||
HttpStatus.UNAUTHORIZED.value());
|
||||
} else {
|
||||
return genInboundErrorResponse(adapterGroupName, callProp, httpProp, adptMsgType, encode, e,
|
||||
HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 표준 -> 비표준 거래
|
||||
* 아웃바운드 어댑터에서 Exception이 발생했을 때, Outbound Process 응답 메시지 생성
|
||||
*/
|
||||
@Override
|
||||
public Object generateOutboundErrorResponseMessage(String outboundadapterGroupName,
|
||||
String outboundadapterName, Properties callProp, Object outboundRequestData, EAIMessage resEaiMsg)
|
||||
throws Exception {
|
||||
String templateKey = outboundadapterGroupName + ".template";
|
||||
String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
|
||||
|
||||
if (StringUtils.isBlank(template))
|
||||
template = PropManager.getInstance().getProperty(PROP_GROUP, "default.template");
|
||||
|
||||
if (StringUtils.isBlank(template)) {
|
||||
return null;
|
||||
}
|
||||
return render(template, resEaiMsg.getStandardMessage(), callProp);
|
||||
}
|
||||
|
||||
private Object genInboundErrorResponse(String adapterGroupName, Properties callProp, Properties httpProp,
|
||||
String adptMsgType, String encode, Throwable e1, int httpCode) {
|
||||
String templateKey = adapterGroupName + ".in." + httpCode + ".template";
|
||||
String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
|
||||
if (StringUtils.isBlank(template)) {
|
||||
templateKey = "default.in." + httpCode + ".template";
|
||||
template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(template)) {
|
||||
templateKey = adapterGroupName + ".in.template";
|
||||
template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(template)) {
|
||||
templateKey = "default.in.template";
|
||||
template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(template)) {
|
||||
String errorResponseFormat = httpProp.getProperty("ERROR_RESPONSE_FORMAT");
|
||||
return MessageUtil.makeErrorMessageByMessageType(adptMsgType, encode,
|
||||
MessageUtil.ERROR_CODE_AP_ERROR, e1.getMessage(), errorResponseFormat);
|
||||
}
|
||||
|
||||
return render(template, null, callProp, e1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 리플렉션으로 예외 객체의 필드 값을 동적으로 조회한다.
|
||||
* 탐색 순서: getXxx() getter → isXxx() getter → 필드 직접 접근
|
||||
* 상위 클래스(Throwable 포함)까지 계층적으로 탐색한다.
|
||||
*/
|
||||
private String resolveExceptionProperty(Throwable e, String fieldName) {
|
||||
if (StringUtils.isBlank(fieldName)) return null;
|
||||
String cap = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
|
||||
for (String prefix : new String[]{"get", "is"}) {
|
||||
Method m = findMethod(e.getClass(), prefix + cap);
|
||||
if (m != null) {
|
||||
try {
|
||||
Object val = m.invoke(e);
|
||||
return val != null ? val.toString() : null;
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
Field f = findField(e.getClass(), fieldName);
|
||||
if (f != null) {
|
||||
try {
|
||||
f.setAccessible(true);
|
||||
Object val = f.get(e);
|
||||
return val != null ? val.toString() : null;
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Method findMethod(Class<?> clazz, String name) {
|
||||
while (clazz != null) {
|
||||
try {
|
||||
Method m = clazz.getDeclaredMethod(name);
|
||||
m.setAccessible(true);
|
||||
return m;
|
||||
} catch (NoSuchMethodException e) {
|
||||
clazz = clazz.getSuperclass();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Field findField(Class<?> clazz, String name) {
|
||||
while (clazz != null) {
|
||||
try {
|
||||
return clazz.getDeclaredField(name);
|
||||
} catch (NoSuchFieldException e) {
|
||||
clazz = clazz.getSuperclass();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
-130
@@ -1,130 +0,0 @@
|
||||
package com.eactive.eai.adapter.handler;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.common.message.EAIMessage;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.JacksonUtil;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
|
||||
public class TemplateCodeConvertAdapterErrorMsgHandler extends TemplateAdapterErrorMsgHandler {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
/** PropManager 에서 코드 변환 설정을 조회할 프로퍼티 그룹 이름 */
|
||||
static final String PROP_GROUP = "AdapterErrorMessageHandler{CODE_CONVERT}";
|
||||
|
||||
// 기본 ObjectMapper 는 응답 JSON 왕복에서 100000000.00 을 1.0E8 로 바꿔버린다.
|
||||
private static final ObjectMapper OBJECT_MAPPER = JacksonUtil.newNumberSafeMapper();
|
||||
|
||||
@Override
|
||||
public Object generateNonStandardErrorResponseMessage(
|
||||
String inboundAdapterGroupName,
|
||||
String inboundAdapterName,
|
||||
Properties callProp,
|
||||
Object outboundRequestData,
|
||||
EAIMessage resEaiMsg) throws Exception {
|
||||
|
||||
Object responseMsessage = super.generateNonStandardErrorResponseMessage(
|
||||
inboundAdapterGroupName,
|
||||
inboundAdapterName,
|
||||
callProp,
|
||||
outboundRequestData,
|
||||
resEaiMsg);
|
||||
|
||||
logger.debug("generateNonStandardErrorResponseMessage - {}", responseMsessage);
|
||||
|
||||
if (!(responseMsessage instanceof String)) {
|
||||
return responseMsessage;
|
||||
}
|
||||
|
||||
String jsonStr = (String) responseMsessage;
|
||||
if (StringUtils.isBlank(jsonStr)) {
|
||||
return responseMsessage;
|
||||
}
|
||||
|
||||
Properties props = PropManager.getInstance().getProperties(PROP_GROUP);
|
||||
if (props == null) {
|
||||
return responseMsessage;
|
||||
}
|
||||
|
||||
String fieldsKey = inboundAdapterGroupName + ".convert.fields";
|
||||
String fieldsValue = props.getProperty(fieldsKey);
|
||||
if (StringUtils.isBlank(fieldsValue)) {
|
||||
return responseMsessage;
|
||||
}
|
||||
|
||||
// 상대 시스템이 개행 등 제어문자를 이스케이프하지 않고 보내는 경우가 있어 정규화 후 파싱한다.
|
||||
// (이미 표준을 지킨 JSON 이면 escapeControlChars 는 원본을 그대로 반환한다)
|
||||
JsonNode rootNode = OBJECT_MAPPER.readTree(JacksonUtil.escapeControlChars(jsonStr));
|
||||
boolean modified = false;
|
||||
|
||||
for (String rawField : fieldsValue.split(",")) {
|
||||
String fieldPath = rawField.trim();
|
||||
if (StringUtils.isBlank(fieldPath)) continue;
|
||||
|
||||
String currentValue = getJsonValue(rootNode, fieldPath);
|
||||
if (currentValue == null) continue;
|
||||
|
||||
// key: inboundAdapterGroupName.{fieldPath}.{현재값} → 폴백: ...{fieldPath}.default
|
||||
String mappingKey = inboundAdapterGroupName + "." + fieldPath + "." + currentValue;
|
||||
String newValue = props.getProperty(mappingKey);
|
||||
if (newValue == null) {
|
||||
String defaultKey = inboundAdapterGroupName + "." + fieldPath + ".default";
|
||||
newValue = props.getProperty(defaultKey);
|
||||
}
|
||||
if (newValue == null) continue;
|
||||
|
||||
setJsonValue(rootNode, fieldPath, newValue);
|
||||
modified = true;
|
||||
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("code convert: field={}, {} -> {}", fieldPath, currentValue, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
responseMsessage = OBJECT_MAPPER.writeValueAsString(rootNode);
|
||||
}
|
||||
|
||||
return responseMsessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* dot 구분 경로로 JsonNode 에서 값을 꺼낸다.
|
||||
* 최종 노드가 값 노드(primitive)가 아니면 null 반환.
|
||||
*/
|
||||
private String getJsonValue(JsonNode node, String fieldPath) {
|
||||
JsonNode current = node;
|
||||
for (String part : fieldPath.split("\\.", -1)) {
|
||||
if (current == null || !current.isObject()) return null;
|
||||
current = current.get(part);
|
||||
}
|
||||
if (current == null || current.isNull() || !current.isValueNode()) return null;
|
||||
return current.asText();
|
||||
}
|
||||
|
||||
/**
|
||||
* dot 구분 경로로 ObjectNode 의 최종 필드 값을 newValue 로 교체한다.
|
||||
* 중간 경로가 ObjectNode 가 아니면 무시한다.
|
||||
*/
|
||||
private void setJsonValue(JsonNode rootNode, String fieldPath, String newValue) {
|
||||
String[] parts = fieldPath.split("\\.", -1);
|
||||
JsonNode current = rootNode;
|
||||
for (int i = 0; i < parts.length - 1; i++) {
|
||||
if (current == null || !current.isObject()) return;
|
||||
current = current.get(parts[i]);
|
||||
}
|
||||
if (current instanceof ObjectNode) {
|
||||
((ObjectNode) current).put(parts[parts.length - 1], newValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -41,12 +41,6 @@ public class HttpStatusException extends Exception {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public HttpStatusException(String msg, String code, int status, Throwable cause) {
|
||||
super(msg, cause);
|
||||
this.setCode(code);
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
@@ -57,7 +51,7 @@ public class HttpStatusException extends Exception {
|
||||
|
||||
public String getCode() {
|
||||
if (StringUtils.isBlank(code)) {
|
||||
return String.format("HttCd:%d", status);
|
||||
return String.format("Http Status: %d", status);
|
||||
} else {
|
||||
return code;
|
||||
}
|
||||
|
||||
+6
-58
@@ -11,9 +11,7 @@ import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
@@ -32,7 +30,6 @@ import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuil
|
||||
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
|
||||
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
|
||||
import org.apache.hc.client5.http.ssl.TrustAllStrategy;
|
||||
import org.apache.hc.core5.http.Header;
|
||||
import org.apache.hc.core5.http.HttpRequest;
|
||||
import org.apache.hc.core5.http.HttpRequestInterceptor;
|
||||
import org.apache.hc.core5.http.HttpResponseInterceptor;
|
||||
@@ -43,10 +40,7 @@ import com.eactive.eai.adapter.AdapterManager;
|
||||
import com.eactive.eai.adapter.Keys;
|
||||
import com.eactive.eai.adapter.http.secure.HttpClient5SSLContextFactory;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.logger.HttpAdapterExtraHeaderVo;
|
||||
import com.eactive.eai.common.message.EAIMessageKeys;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.server.EAIServerManager;
|
||||
import com.eactive.eai.common.util.HttpAdapterExtraLogUtil;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.httpouttlsinfo.HttpOutTlsInfoVO;
|
||||
@@ -59,8 +53,6 @@ public abstract class HttpClient5AdapterServiceSupport implements HttpClientAdap
|
||||
|
||||
private static boolean testMode = TestModeChecker.isTestMode();
|
||||
|
||||
private static boolean httpHeaderLogMode = HttpAdapterExtraLogUtil.isHttpHeaderMode();
|
||||
|
||||
public synchronized void close() {
|
||||
if(connectionManager != null) {
|
||||
connectionManager.close();
|
||||
@@ -374,20 +366,8 @@ public abstract class HttpClient5AdapterServiceSupport implements HttpClientAdap
|
||||
}
|
||||
else {
|
||||
sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build();
|
||||
SSLConnectionSocketFactory sslSocketFactory = null;
|
||||
if(testMode) {
|
||||
// Hostname verifier 비활성화 (테스트용)
|
||||
sslSocketFactory = new SSLConnectionSocketFactory(
|
||||
sslContext
|
||||
, NoopHostnameVerifier.INSTANCE
|
||||
);
|
||||
}
|
||||
else {
|
||||
sslSocketFactory = new SSLConnectionSocketFactory(
|
||||
sslContext
|
||||
);
|
||||
}
|
||||
cmBuilder.setSSLSocketFactory(sslSocketFactory);
|
||||
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext);
|
||||
cmBuilder.setSSLSocketFactory(csf);
|
||||
}
|
||||
|
||||
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException
|
||||
@@ -404,11 +384,6 @@ public abstract class HttpClient5AdapterServiceSupport implements HttpClientAdap
|
||||
|
||||
|
||||
HttpRequestInterceptor requestLogInterceptor = (request, entity, context) -> {
|
||||
if (!httpHeaderLogMode) {
|
||||
logger.info("httpHeader logging off - use.http.header.log");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
String uuid = (String) context.getAttribute(TransactionContextKeys.TRANSACTION_UUID);
|
||||
String contextAdapterGroupName = (String) context.getAttribute(HttpClientAdapterServiceKey.ADAPTER_GROUP_NAME);
|
||||
@@ -420,36 +395,15 @@ public abstract class HttpClient5AdapterServiceSupport implements HttpClientAdap
|
||||
logProcessNo = 200;
|
||||
}
|
||||
|
||||
Header[] headers = request.getHeaders();
|
||||
List<HttpAdapterExtraHeaderVo> headerVoList = null;
|
||||
if ( headers != null && headers.length != 0 ) {
|
||||
headerVoList = HttpAdapterExtraLogUtil.convertHeaderToListOfHttpAdapterExtraHeaderVo(headers);
|
||||
} else {
|
||||
headerVoList = new ArrayList<HttpAdapterExtraHeaderVo>();
|
||||
}
|
||||
|
||||
String trimedPostBody = (String) context.getAttribute(HttpAdapterExtraLogUtil.BODY_FIELD_NAME);
|
||||
if(StringUtils.isNotEmpty(trimedPostBody)) {
|
||||
HttpAdapterExtraHeaderVo vo = new HttpAdapterExtraHeaderVo();
|
||||
vo.setName(HttpAdapterExtraLogUtil.BODY_FIELD_NAME);
|
||||
vo.setValue(trimedPostBody);
|
||||
headerVoList.add(vo);
|
||||
}
|
||||
|
||||
HttpAdapterExtraLogUtil.insertHttpAdapterExtraLog(uuid, logProcessNo,
|
||||
contextAdapterGroupName, contextAdapterName, headerVoList, url, request.getMethod(), 0);
|
||||
HttpAdapterExtraLogUtil.insertHttpAdapterExtraLog(uuid, logProcessNo,
|
||||
contextAdapterGroupName, contextAdapterName, request.getHeaders(), url, request.getMethod());
|
||||
} catch (URISyntaxException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
};
|
||||
|
||||
HttpResponseInterceptor responseLogInterceptor = (response, entity, context) -> {
|
||||
if (!httpHeaderLogMode) {
|
||||
logger.info("httpHeader logging off - use.http.header.log");
|
||||
return;
|
||||
}
|
||||
|
||||
try{
|
||||
try{
|
||||
HttpRequest request = (HttpRequest)context.getAttribute("http.request");
|
||||
String url = request.getUri().toString();
|
||||
String uuid = (String) context.getAttribute(TransactionContextKeys.TRANSACTION_UUID);
|
||||
@@ -463,7 +417,7 @@ public abstract class HttpClient5AdapterServiceSupport implements HttpClientAdap
|
||||
}else{
|
||||
logProcessNo += 100;
|
||||
}
|
||||
|
||||
|
||||
HttpAdapterExtraLogUtil.insertHttpAdapterExtraLog(uuid, logProcessNo,
|
||||
contextAdapterGroupName, contextAdapterName, response.getHeaders(), url, request.getMethod(), response.getCode());
|
||||
} catch (URISyntaxException e) {
|
||||
@@ -588,10 +542,4 @@ public abstract class HttpClient5AdapterServiceSupport implements HttpClientAdap
|
||||
}
|
||||
|
||||
public abstract Object execute(Properties prop, Object message, Properties tempProp) throws Exception;
|
||||
|
||||
protected boolean isTas(Properties tempProp) {
|
||||
String tranType = tempProp.getProperty("tranType");
|
||||
return EAIServerManager.getInstance().isTASEnabledEAIServer()
|
||||
&& StringUtils.equals(tranType, EAIMessageKeys.TRANTYPE_TAS);
|
||||
}
|
||||
}
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import org.apache.hc.core5.http.ContentType;
|
||||
import org.apache.hc.core5.http.NameValuePair;
|
||||
import org.apache.hc.core5.http.io.entity.StringEntity;
|
||||
import org.apache.hc.core5.net.WWWFormCodec;
|
||||
|
||||
/**
|
||||
* HttpComponent5에서 지원하는 UrlEncodedFormEntity 클래스는 chunked 옵션 설정을 지원하지 않아 새롭게 만들었음.
|
||||
*/
|
||||
public class CustomUrlEncodedFormEntity extends StringEntity {
|
||||
|
||||
public CustomUrlEncodedFormEntity(final Iterable<? extends NameValuePair> parameters, final Charset charset,
|
||||
final boolean chunked) {
|
||||
super(WWWFormCodec.format(parameters,
|
||||
charset != null ? charset : ContentType.APPLICATION_FORM_URLENCODED.getCharset()),
|
||||
charset != null ? ContentType.APPLICATION_FORM_URLENCODED.withCharset(charset)
|
||||
: ContentType.APPLICATION_FORM_URLENCODED,
|
||||
chunked);
|
||||
}
|
||||
|
||||
}
|
||||
+8
-8
@@ -88,17 +88,10 @@ public class DeadlineAwareRetryExecutor {
|
||||
|
||||
if (!policy.shouldRetryOnStatus(request, status)) {
|
||||
// 성공 또는 재시도 불필요 → 호출부에서 close 책임
|
||||
log.info("non-retryable response status={}, attempt={}", status, attempt + 1);
|
||||
log.debug("non-retryable response status={}, attempt={}", status, attempt + 1);
|
||||
return response;
|
||||
}
|
||||
|
||||
// ④ 마지막 시도였으면 종료
|
||||
if (attempt >= policy.getMaxRetries()) {
|
||||
// 최대 재처리에 도달하여, 재시도하지 않고, 응답 객체를 리턴한다.
|
||||
log.info("최대 재시도 횟수 도달 response status={}, attempt={}", status, attempt + 1);
|
||||
return response;
|
||||
}
|
||||
|
||||
// 재시도 대상
|
||||
log.warn("retryable status={}, attempt={}/{}",
|
||||
status, attempt + 1, policy.getMaxRetries());
|
||||
@@ -134,6 +127,9 @@ public class DeadlineAwareRetryExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
// ④ 마지막 시도였으면 바로 종료
|
||||
if (attempt >= policy.getMaxRetries()) break;
|
||||
|
||||
// ⑤ 백오프 대기 전 남은 시간 재확인
|
||||
long remainingBeforeWait = totalDeadlineMs - (System.currentTimeMillis() - startTime);
|
||||
if (remainingBeforeWait <= policy.getBackoffMs()) {
|
||||
@@ -175,6 +171,10 @@ public class DeadlineAwareRetryExecutor {
|
||||
log.info("RequestConfig = {}", config);
|
||||
}
|
||||
|
||||
private boolean isSuccess(int status) {
|
||||
return status >= 200 && status < 300;
|
||||
}
|
||||
|
||||
private void closeQuietly(CloseableHttpResponse response) {
|
||||
try {
|
||||
response.close();
|
||||
|
||||
+11
-26
@@ -7,15 +7,11 @@ import com.eactive.eai.adapter.http.client.HttpClientAdapterVO;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.message.MessageType;
|
||||
import com.eactive.eai.common.util.CommonLib;
|
||||
import com.eactive.eai.common.util.JacksonUtil;
|
||||
import com.eactive.eai.common.util.TxFileLogger;
|
||||
|
||||
import org.apache.commons.httpclient.HttpStatus;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.time.StopWatch;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpPost;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
|
||||
import org.apache.hc.core5.http.ContentType;
|
||||
import org.apache.hc.core5.http.HttpHost;
|
||||
@@ -45,11 +41,14 @@ public class HttpClient5AdapterServiceBody extends HttpClient5AdapterServiceSupp
|
||||
boolean useForwardProxy = StringUtils.equalsIgnoreCase(prop.getProperty("FORWARD_PROXY_USE_YN", "N"), "Y");
|
||||
String forwardProxyUrl = prop.getProperty("FORWARD_PROXY_URL");
|
||||
|
||||
if(useForwardProxy && !isTas(tempProp)) {
|
||||
if(useForwardProxy) {
|
||||
java.net.URL url = new java.net.URL(forwardProxyUrl);
|
||||
|
||||
// 프로토콜, 호스트, 포트 추출
|
||||
HttpHost proxy = new HttpHost(url.getProtocol(), url.getHost(), url.getPort());
|
||||
String protocol = url.getProtocol();
|
||||
String host = url.getHost();
|
||||
int port = url.getPort();
|
||||
HttpHost proxy = new HttpHost(protocol,host, port);
|
||||
requestConfigBuilder.setProxy(proxy);
|
||||
}
|
||||
|
||||
@@ -61,10 +60,8 @@ public class HttpClient5AdapterServiceBody extends HttpClient5AdapterServiceSupp
|
||||
}
|
||||
|
||||
if(MessageType.JSON.equals(prop.getProperty("messageType"))) {
|
||||
// 파싱 후 재직렬화하므로 숫자 자릿수가 유실되지 않는 mapper 를 쓴다.
|
||||
// 기본 ObjectMapper 는 100000000.00 을 1.0E8 로 바꿔버린다.
|
||||
ObjectMapper mapper = JacksonUtil.newNumberSafeMapper();
|
||||
ObjectNode jsonNode = (ObjectNode) mapper.readTree(JacksonUtil.escapeControlChars(sendData));
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
ObjectNode jsonNode = (ObjectNode) mapper.readTree(sendData);
|
||||
ObjectNode headerPart = (ObjectNode) jsonNode.get("header_part");
|
||||
|
||||
if( headerPart.get("mciIntfId") != null && headerPart.get("mciIntfId").asText().trim().length() > 0 ) {
|
||||
@@ -124,9 +121,7 @@ public class HttpClient5AdapterServiceBody extends HttpClient5AdapterServiceSupp
|
||||
HttpMemoryLogger.txlog(vo.getAdapterGroupName() + vo.getAdapterName(),
|
||||
"SEND [" + sendData + "]" + CommonLib.getDumpMessage(sendData));
|
||||
}
|
||||
|
||||
TxFileLogger.logTxFile(tempProp, sendData, "[OUT_SEND]");
|
||||
|
||||
|
||||
String uuid = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID);
|
||||
HttpContext context = new BasicHttpContext();
|
||||
context.setAttribute(TransactionContextKeys.TRANSACTION_UUID, uuid);
|
||||
@@ -139,15 +134,7 @@ public class HttpClient5AdapterServiceBody extends HttpClient5AdapterServiceSupp
|
||||
|
||||
byte[] responseMessage = null;
|
||||
String responseString = "";
|
||||
|
||||
DeadlineAwareRetryExecutor executor =
|
||||
new DeadlineAwareRetryExecutor((CloseableHttpClient)this.client,
|
||||
vo.getConnectionTimeout(),
|
||||
vo.getTimeout());
|
||||
|
||||
// Properties에서 RetryPolicy 로드
|
||||
RetryPolicy policy = RetryPolicy.from(prop); // 설정 로드 방식에 맞게
|
||||
try (CloseableHttpResponse response = (CloseableHttpResponse) executor.execute(method, context, policy)) {
|
||||
try (CloseableHttpResponse response = (CloseableHttpResponse) this.client.execute(method, context)) {
|
||||
status = response.getCode();
|
||||
responseMessage = EntityUtils.toByteArray(response.getEntity());
|
||||
responseString = new String(responseMessage, vo.getEncode());
|
||||
@@ -163,8 +150,7 @@ public class HttpClient5AdapterServiceBody extends HttpClient5AdapterServiceSupp
|
||||
}
|
||||
|
||||
stopWatch.stop();
|
||||
TxFileLogger.logTxFile(tempProp, responseString, "[OUT_RECV]");
|
||||
|
||||
|
||||
|
||||
if (status >= 400 && status < 500) {
|
||||
String errMsg = String.format(
|
||||
@@ -189,8 +175,7 @@ public class HttpClient5AdapterServiceBody extends HttpClient5AdapterServiceSupp
|
||||
+ CommonLib.getDumpMessage(responseMessage));
|
||||
}
|
||||
|
||||
// return responseMessage;
|
||||
return responseString;
|
||||
return responseMessage;
|
||||
|
||||
} catch (SocketTimeoutException ste) { // Read Timeout :: HttpClient 3.1일 경우
|
||||
if (vo.getTraceLevel() >= 3) {
|
||||
|
||||
-679
@@ -1,679 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ConnectException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.BitSet;
|
||||
import java.util.Formatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.time.StopWatch;
|
||||
import org.apache.hc.client5.http.classic.HttpClient;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpDelete;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpGet;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpPost;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpPut;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
|
||||
import org.apache.hc.core5.http.Header;
|
||||
import org.apache.hc.core5.http.HttpStatus;
|
||||
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
|
||||
import org.apache.hc.core5.http.io.entity.EntityUtils;
|
||||
import org.apache.hc.core5.http.protocol.BasicHttpContext;
|
||||
import org.apache.hc.core5.http.protocol.HttpContext;
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import com.eactive.eai.adapter.http.HttpMemoryLogger;
|
||||
import com.eactive.eai.adapter.http.HttpMethodType;
|
||||
import com.eactive.eai.adapter.http.client.HttpClient5AdapterServiceSupport;
|
||||
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey;
|
||||
import com.eactive.eai.adapter.http.client.HttpClientAdapterVO;
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.adapter.http.dynamic.impl.HttpAdapterServiceBypass;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.exception.ExceptionUtil;
|
||||
import com.eactive.eai.common.util.CommonLib;
|
||||
import com.jayway.jsonpath.DocumentContext;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.openbanking.eai.common.token.AccessTokenManager;
|
||||
import com.openbanking.eai.common.token.AccessTokenVO;
|
||||
import com.openbanking.eai.common.token.OAuth2AccessTokenVO;
|
||||
|
||||
public class HttpClient5AdapterServiceBypass extends HttpClient5AdapterServiceSupport
|
||||
implements HttpClientAdapterServiceKey {
|
||||
public static final String TYPE_BYPASS_REQUEST = "bypassRequest";
|
||||
public static final String REST_OPTION = "REST_OPTION";
|
||||
|
||||
protected boolean doSendUrlFragment = true;
|
||||
protected boolean doHandleCompression = false;
|
||||
protected boolean doForwardIP = false;
|
||||
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
public Object execute(Properties prop, Object data, Properties tempProp) throws Exception {
|
||||
HttpClientAdapterVO vo = super.setting(prop, tempProp);
|
||||
|
||||
boolean useAdapterToken = StringUtils.equalsIgnoreCase(prop.getProperty("ADAPTER_TOKEN_USE_YN", "N"), "Y");
|
||||
|
||||
// ex) $.dataHeader.GW_RSLT_CD
|
||||
String tokenErrorCodeKey = prop.getProperty("ADAPTER_TOKEN_ERROR_CODE_KEY");
|
||||
String tokenErrorCodeValues = prop.getProperty("ADAPTER_TOKEN_ERROR_CODE_VALUES");
|
||||
String tokenErrorHttpStatusCode = prop.getProperty("ADAPTER_TOKEN_ERROR_HTTP_STATUS_CODE"); // 200 or 400번대
|
||||
|
||||
/**
|
||||
* @formatter:off
|
||||
* TSEAIHE02.RESTOPTION 정보 => JSON 형태로 구성
|
||||
* - type : simpleRequest, variableUrlRequest
|
||||
* - extraPath : 어댑터 프로퍼티 URL에 추가될 HTTP REST URL
|
||||
* - method : get, delete, post, put
|
||||
* - contentType : application/json, application/x-www-form-urlencoded
|
||||
* - adapterTokenUseYn: Y, N(default)
|
||||
* ex)
|
||||
* {"type":"variableUrlRequest","extraPath":"/aaa/{userId}","uriVariables":["userId"]}
|
||||
* @formatter:on
|
||||
*/
|
||||
String restOptionData = tempProp.getProperty(REST_OPTION, "{}");
|
||||
|
||||
JSONObject restOptionObject = parseJson(restOptionData);
|
||||
|
||||
String adapterTokenUseYn = (String) restOptionObject.get("adapterTokenUseYn");
|
||||
if (StringUtils.isNotBlank(adapterTokenUseYn)) { // interface 에 설정된게 우선한다.
|
||||
useAdapterToken = StringUtils.equalsIgnoreCase(adapterTokenUseYn, "Y");
|
||||
}
|
||||
|
||||
String inboundMethod = tempProp.getProperty(HttpAdapterServiceKey.INBOUND_METHOD, "POST");
|
||||
String inboundRewritePath = tempProp.getProperty(HttpAdapterServiceKey.INBOUND_REWRITE_PATH, "");
|
||||
String inboundQueryString = tempProp.getProperty(HttpAdapterServiceKey.INBOUND_QUERY_STRING, "");
|
||||
Properties inboundHeaders = (Properties) tempProp.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
Map<String, String> inboundPathVariables = (Map<String, String>) tempProp
|
||||
.get(HttpAdapterServiceKey.INBOUND_PATH_VARIABLES);
|
||||
|
||||
String restMethod = (String) restOptionObject.get("method");
|
||||
if (StringUtils.isBlank(restMethod)) {
|
||||
restMethod = inboundMethod;
|
||||
}
|
||||
|
||||
String url = getRewriteUrl(vo, inboundRewritePath, inboundQueryString, restOptionObject, inboundPathVariables);
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("HttpClient5AdapterServiceBypass] url = [" + url + "]");
|
||||
}
|
||||
|
||||
HttpClient mclient = this.client;
|
||||
HttpUriRequestBase method = generateMethod(restMethod, url);
|
||||
|
||||
assignRequestHeaders(method, inboundHeaders, tempProp);
|
||||
|
||||
if (hasBody(data, method)) {
|
||||
byte[] bodyBytes;
|
||||
if (data instanceof byte[]) {
|
||||
bodyBytes = (byte[]) data;
|
||||
} else if (data instanceof String) {
|
||||
bodyBytes = ((String) data).getBytes(vo.getEncode());
|
||||
} else {
|
||||
bodyBytes = new byte[0];
|
||||
}
|
||||
method.setEntity(new ByteArrayEntity(bodyBytes, null));
|
||||
}
|
||||
|
||||
String contentType = (String) restOptionObject.get("contentType");
|
||||
if (StringUtils.isBlank(contentType) && inboundHeaders != null) {
|
||||
contentType = getIgnoreCaseProp(inboundHeaders, HttpHeaders.CONTENT_TYPE);
|
||||
contentType = StringUtils.substringBefore(contentType, ";");
|
||||
}
|
||||
|
||||
RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
|
||||
configureHttpClient(requestConfigBuilder, vo, method);
|
||||
|
||||
OAuth2AccessTokenVO accessToken = null;
|
||||
if (useAdapterToken) {
|
||||
AccessTokenManager tokenManager = AccessTokenManager.getInstance();
|
||||
accessToken = (OAuth2AccessTokenVO) tokenManager.getAccessTokenVO(vo.getAdapterGroupName());
|
||||
|
||||
// 토큰이 없거나 만료 됐으면 재발급
|
||||
if (accessToken == null || accessToken.isExpired()) {
|
||||
String oldToken = accessToken == null ? null : accessToken.getAccessToken();
|
||||
accessToken = (OAuth2AccessTokenVO) tokenManager.retryAccessTokenVO(vo.getAdapterGroupName(), prop,
|
||||
oldToken);
|
||||
}
|
||||
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("HttpClient5AdapterServiceBypass] SEND (" + vo.getAdapterGroupName() + ") TOKEN = ["
|
||||
+ accessToken + "]");
|
||||
}
|
||||
|
||||
setAuthHeaders(method, accessToken);
|
||||
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("HttpClient5AdapterServiceBypass] SEND (" + vo.getAdapterGroupName()
|
||||
+ ") RequestHeader [Authorization=" + method.getFirstHeader("Authorization") + "]");
|
||||
}
|
||||
}
|
||||
|
||||
// 로깅 인터셉터에 전달할 컨텍스트 정보 설정
|
||||
String uuid = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID);
|
||||
HttpContext context = new BasicHttpContext();
|
||||
context.setAttribute(TransactionContextKeys.TRANSACTION_UUID, uuid);
|
||||
context.setAttribute(HttpClientAdapterServiceKey.ADAPTER_GROUP_NAME, vo.getAdapterGroupName());
|
||||
context.setAttribute(HttpClientAdapterServiceKey.ADAPTER_NAME, vo.getAdapterName());
|
||||
Integer logProcessNo = (Integer) tempProp.get(HttpClientAdapterServiceKey.LOG_PROCESS_NO);
|
||||
if (logProcessNo != null && logProcessNo > 0) {
|
||||
context.setAttribute(HttpClientAdapterServiceKey.LOG_PROCESS_NO, logProcessNo);
|
||||
}
|
||||
|
||||
int status = -1;
|
||||
|
||||
try {
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
stopWatch.start();
|
||||
|
||||
byte[] responseMessage = null;
|
||||
Header[] responseHeaders = null;
|
||||
|
||||
if (vo.getTraceLevel() >= 3) {
|
||||
HttpMemoryLogger.txlog(vo.getAdapterGroupName() + vo.getAdapterName(),
|
||||
"SEND [Bypass Request..]" + CommonLib.getDumpMessage(data));
|
||||
}
|
||||
try {
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("[method getName]" + method.getMethod());
|
||||
logger.debug("[method getRequestHeaders]" + java.util.Arrays.toString(method.getHeaders()));
|
||||
logger.debug("[method getURI]" + method.getPath());
|
||||
}
|
||||
try (CloseableHttpResponse response = (CloseableHttpResponse) mclient.execute(method, context)) {
|
||||
status = response.getCode();
|
||||
responseHeaders = response.getHeaders();
|
||||
responseMessage = EntityUtils.toByteArray(response.getEntity());
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("[received status]" + status);
|
||||
}
|
||||
}
|
||||
} catch (ConnectException e) {
|
||||
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
|
||||
logger.debug("HttpClient5AdapterServiceBypass] responseType =" + vo.getResponseType());
|
||||
logger.debug("HttpClient5AdapterServiceBypass] RECV (" + vo.getAdapterGroupName() + ") = "
|
||||
+ CommonLib.getDumpMessage(responseMessage));
|
||||
}
|
||||
throw new Exception("excuteMethod java.net.ConnectException = " + e.getMessage());
|
||||
}
|
||||
|
||||
stopWatch.stop();
|
||||
|
||||
/**
|
||||
* @formatter:off
|
||||
* OAuth 토큰 응답 체크(Adapter properties로 설정)
|
||||
* 응답코드에 따라 유효한 토큰 확인(토큰 재발급 여부 확인)
|
||||
* API 서비스 마다 정책이 다름(보통 400대에서 체크하나 200에서도 체크할 수 있음)
|
||||
*
|
||||
* TOKEN_ERROR_CODE_KEY: 응답 json error code key
|
||||
* __ex) $.dataHeader.resultCode
|
||||
* TOKEN_ERROR_CODE_VALUES: 토큰 재발급이 필요한 응답 error codes(ex: O0001,O0002,O0003)
|
||||
* TOKEN_ERROR_HTTP_STATUS_CODE: 보통 400대에서 체크하나 API 제공자에 따라 200에서도 체클할수 있음
|
||||
* ex) TOKEN_ERROR_HTTP_STATUS_CODE = 200
|
||||
* @formatter:on
|
||||
*/
|
||||
boolean needReissue = false;
|
||||
|
||||
if (status == 200) {
|
||||
if (useAdapterToken && StringUtils.equals(tokenErrorHttpStatusCode, "200")) {
|
||||
needReissue = checkTokenRetry(responseMessage, tokenErrorCodeKey, tokenErrorCodeValues,
|
||||
vo.getEncode());
|
||||
}
|
||||
} else if (status >= 400 && status < 500) {
|
||||
if (useAdapterToken) {
|
||||
needReissue = checkTokenRetry(responseMessage, tokenErrorCodeKey, tokenErrorCodeValues,
|
||||
vo.getEncode());
|
||||
}
|
||||
|
||||
if (!needReissue) {
|
||||
// body 값이 없을때만 exception 처리하고, body가 있으면 bypass 한다.
|
||||
if (responseMessage == null || responseMessage.length == 0) {
|
||||
String errMsg = String.format(
|
||||
"http receive status fail value=%d adapterName=%s.%s uri=%s method=%s response Data=%s",
|
||||
status, vo.getAdapterGroupName(), vo.getAdapterName(), url, restMethod, responseMessage);
|
||||
throw new Exception(errMsg);
|
||||
}
|
||||
}
|
||||
} else if (status != 200) {
|
||||
responseMessage = responseMessage != null ? responseMessage : new byte[0];
|
||||
// body 값이 없을때만 exception 처리하고, body가 있으면 bypass 한다.
|
||||
if (responseMessage.length == 0) {
|
||||
String errMsg = String.format(
|
||||
"http receive status fail value=%d adapterName=%s.%s uri=%s method=%s response Data=%s",
|
||||
status, vo.getAdapterGroupName(), vo.getAdapterName(), url, restMethod, responseMessage);
|
||||
throw new Exception(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
if (useAdapterToken) {
|
||||
if (responseMessage == null) {
|
||||
throw new Exception("responseMessage is NULL, HttpStatus=" + status);
|
||||
}
|
||||
|
||||
// OAuth 토큰 재요청 코드 확인
|
||||
if (needReissue) {
|
||||
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
|
||||
logger.debug("HttpClient5AdapterServiceBypass] retry access token response code = ["
|
||||
+ vo.getResponseType() + "] message = [" + new String(responseMessage, vo.getEncode())
|
||||
+ "]");
|
||||
}
|
||||
|
||||
// 토큰 재발급
|
||||
AccessTokenManager tokenManager = AccessTokenManager.getInstance();
|
||||
String oldToken = accessToken == null ? null : accessToken.getAccessToken();
|
||||
OAuth2AccessTokenVO newAccessToken = (OAuth2AccessTokenVO) tokenManager
|
||||
.retryAccessTokenVO(vo.getAdapterGroupName(), prop, oldToken);
|
||||
method.setHeader("Authorization", newAccessToken.getAuthorization());
|
||||
setAuthHeaders(method, newAccessToken);
|
||||
|
||||
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
|
||||
logger.debug("HttpClient5AdapterServiceBypass] SEND (" + vo.getAdapterGroupName()
|
||||
+ ") RETRY TOKEN = [" + newAccessToken + "]");
|
||||
}
|
||||
|
||||
try (CloseableHttpResponse retryResponse = (CloseableHttpResponse) mclient.execute(method, context)) {
|
||||
status = retryResponse.getCode();
|
||||
responseHeaders = retryResponse.getHeaders();
|
||||
responseMessage = EntityUtils.toByteArray(retryResponse.getEntity());
|
||||
} catch (IOException e) {
|
||||
throw new Exception("retry excuteMethod Exception = " + e.getMessage());
|
||||
}
|
||||
|
||||
if (status != 200) {
|
||||
responseMessage = responseMessage != null ? responseMessage : new byte[0];
|
||||
if (responseMessage.length == 0) {
|
||||
String errMsg = String.format(
|
||||
"http receive status fail value=%d adapterName=%s.%s uri=%s method=%s response Data=%s",
|
||||
status, vo.getAdapterGroupName(), vo.getAdapterName(), url, restMethod,
|
||||
responseMessage);
|
||||
throw new Exception(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
|
||||
logger.debug("HttpClient5AdapterServiceBypass] RETRY responseType =" + vo.getResponseType());
|
||||
logger.debug("HttpClient5AdapterServiceBypass] RETRY RECV (" + vo.getAdapterGroupName() + ") = "
|
||||
+ CommonLib.getDumpMessage(responseMessage));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (vo.getTraceLevel() >= 3) {
|
||||
HttpMemoryLogger.txlog(vo.getAdapterGroupName() + vo.getAdapterName(),
|
||||
"RECV [Bypass Request..]" + CommonLib.getDumpMessage(responseMessage));
|
||||
}
|
||||
// 응답 Content-Type charset 기반 인코딩 결정, 없으면 어댑터 encoding 사용
|
||||
String responseEncode = vo.getEncode();
|
||||
if (responseHeaders != null) {
|
||||
for (Header header : responseHeaders) {
|
||||
if (StringUtils.equalsIgnoreCase(header.getName(), HttpHeaders.CONTENT_TYPE)) {
|
||||
try {
|
||||
MediaType mediaType = MediaType.parseMediaType(header.getValue());
|
||||
if (mediaType.getCharset() != null) {
|
||||
responseEncode = mediaType.getCharset().name();
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
assignRelayDataToInbound(tempProp, responseHeaders, status);
|
||||
return responseMessage != null ? new String(responseMessage, responseEncode) : "";
|
||||
} catch (SocketTimeoutException ste) {
|
||||
if (vo.getTraceLevel() >= 3) {
|
||||
HttpMemoryLogger.error(vo.getAdapterGroupName() + vo.getAdapterName(),
|
||||
"HttpClient5AdapterServiceBypass] SocketTimeoutException (" + vo.getAdapterGroupName() + ") : "
|
||||
+ ste.toString(),
|
||||
ste);
|
||||
}
|
||||
logger.error("HttpClient5AdapterServiceBypass] SocketTimeoutException (" + vo.getAdapterGroupName() + ") : "
|
||||
+ ste.toString(), ste);
|
||||
throw ste;
|
||||
} catch (ConnectException ce) {
|
||||
if (vo.getTraceLevel() >= 3) {
|
||||
HttpMemoryLogger.error(vo.getAdapterGroupName() + vo.getAdapterName(),
|
||||
"HttpClient5AdapterServiceBypass] Connection Exception (" + vo.getAdapterGroupName() + ") : "
|
||||
+ ce.toString(),
|
||||
ce);
|
||||
}
|
||||
logger.error("HttpClient5AdapterServiceBypass] Connection Exception (" + vo.getAdapterGroupName() + ") : "
|
||||
+ ce.toString(), ce);
|
||||
throw ce;
|
||||
} catch (Exception e) {
|
||||
if (vo.getTraceLevel() >= 3) {
|
||||
HttpMemoryLogger.error(vo.getAdapterGroupName() + vo.getAdapterName(), e.toString(), e);
|
||||
}
|
||||
logger.error(
|
||||
"HttpClient5AdapterServiceBypass] Exception (" + vo.getAdapterGroupName() + ") : " + e.toString(),
|
||||
e);
|
||||
throw e;
|
||||
} finally {
|
||||
if (status != HttpStatus.SC_OK) {
|
||||
String[] msgArgs = new String[1];
|
||||
msgArgs[0] = String.valueOf(status);
|
||||
String resMsg = ExceptionUtil.make("RDCEAIAHA013", msgArgs);
|
||||
if (logger.isDebug()) {
|
||||
logger.debug(resMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasBody(Object data, HttpUriRequestBase method) {
|
||||
if (data == null) {
|
||||
return false;
|
||||
}
|
||||
// GET, DELETE는 표준 HTTP에서 body를 지원하지 않음
|
||||
return method instanceof HttpPost || method instanceof HttpPut;
|
||||
}
|
||||
|
||||
private HttpUriRequestBase generateMethod(String restMethod, String url) {
|
||||
switch (HttpMethodType.getValue(restMethod)) {
|
||||
case GET:
|
||||
return new HttpGet(url);
|
||||
case DELETE:
|
||||
return new HttpDelete(url);
|
||||
case POST:
|
||||
return new HttpPost(url);
|
||||
case PUT:
|
||||
return new HttpPut(url);
|
||||
default:
|
||||
return new HttpPost(url);
|
||||
}
|
||||
}
|
||||
|
||||
private String getRewriteUrl(HttpClientAdapterVO vo, String inboundRewritePath, String queryString,
|
||||
JSONObject restOptionObject, Map<String, String> inboundPathVariables) {
|
||||
String fragment = null;
|
||||
if (StringUtils.isNotBlank(queryString)) {
|
||||
int fragIdx = queryString.indexOf('#');
|
||||
if (fragIdx >= 0) {
|
||||
fragment = queryString.substring(fragIdx + 1);
|
||||
queryString = queryString.substring(0, fragIdx);
|
||||
}
|
||||
}
|
||||
|
||||
String restExtraPath = (String) restOptionObject.get("extraPath");
|
||||
if (StringUtils.isBlank(restExtraPath)) {
|
||||
StringBuilder uri = new StringBuilder(500);
|
||||
String baseUrl = StringUtils.removeEnd(vo.getUrl(), "/");
|
||||
String rewritePath = inboundRewritePath != null && !inboundRewritePath.startsWith("/")
|
||||
? "/" + inboundRewritePath : inboundRewritePath;
|
||||
uri.append(baseUrl).append(StringUtils.defaultString(rewritePath));
|
||||
|
||||
if (StringUtils.isNotBlank(queryString)) {
|
||||
uri.append('?');
|
||||
uri.append(encodeUriQuery(queryString, false));
|
||||
}
|
||||
|
||||
if (doSendUrlFragment && fragment != null) {
|
||||
uri.append('#');
|
||||
uri.append(encodeUriQuery(fragment, false));
|
||||
}
|
||||
|
||||
return uri.toString();
|
||||
} else {
|
||||
String type = (String) restOptionObject.get("type");
|
||||
String url = getUrl(vo.getUrl(), restExtraPath);
|
||||
|
||||
if (StringUtils.equalsIgnoreCase(type, HttpClientAdapterServiceRest.TYPE_VARIABLE_URL_REQUEST)) {
|
||||
inboundPathVariables = mergeInboundPathVariables(inboundPathVariables, queryString);
|
||||
List<String> urlVariableValueList = new ArrayList<>();
|
||||
JSONArray uriVariables = (JSONArray) restOptionObject.get("uriVariables");
|
||||
if (uriVariables != null && !uriVariables.isEmpty() && inboundPathVariables != null
|
||||
&& !inboundPathVariables.isEmpty()) {
|
||||
for (Object tempObject : uriVariables) {
|
||||
String urlVaribleId = (String) tempObject;
|
||||
String uriVariableValue = inboundPathVariables.get(urlVaribleId);
|
||||
urlVariableValueList.add(uriVariableValue);
|
||||
}
|
||||
|
||||
if (!urlVariableValueList.isEmpty()) {
|
||||
UriComponents uriComponents = UriComponentsBuilder.fromUriString(url).build();
|
||||
Object[] urls = urlVariableValueList.toArray();
|
||||
url = uriComponents.expand(urls).toUriString();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (StringUtils.isNotBlank(queryString)) {
|
||||
url += "?" + encodeUriQuery(queryString, false);
|
||||
}
|
||||
|
||||
if (doSendUrlFragment && fragment != null) {
|
||||
url += "#" + encodeUriQuery(fragment, false);
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> mergeInboundPathVariables(Map<String, String> inboundPathVariables,
|
||||
String queryString) {
|
||||
if (StringUtils.isBlank(queryString)) {
|
||||
return inboundPathVariables;
|
||||
}
|
||||
|
||||
if (inboundPathVariables == null) {
|
||||
inboundPathVariables = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
String[] pairs = queryString.split("&");
|
||||
for (String pair : pairs) {
|
||||
int idx = pair.indexOf("=");
|
||||
inboundPathVariables.put(pair.substring(0, idx), pair.substring(idx + 1));
|
||||
}
|
||||
|
||||
return inboundPathVariables;
|
||||
}
|
||||
|
||||
private String getUrl(String baseUrl, String extraPath) {
|
||||
if (StringUtils.isBlank(extraPath)) {
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
if (StringUtils.contains(extraPath, "http://") || StringUtils.contains(extraPath, "https://")) {
|
||||
return extraPath;
|
||||
}
|
||||
|
||||
String targetUrl = baseUrl;
|
||||
if (!targetUrl.endsWith("/")) {
|
||||
targetUrl += "/";
|
||||
}
|
||||
if (extraPath.startsWith("/")) {
|
||||
targetUrl += extraPath.substring(1);
|
||||
} else {
|
||||
targetUrl += extraPath;
|
||||
}
|
||||
|
||||
return targetUrl;
|
||||
}
|
||||
|
||||
protected CharSequence encodeUriQuery(CharSequence in, boolean encodePercent) {
|
||||
StringBuilder outBuf = null;
|
||||
Formatter formatter = null;
|
||||
for (int i = 0; i < in.length(); i++) {
|
||||
char c = in.charAt(i);
|
||||
boolean escape = true;
|
||||
if (c < 128) {
|
||||
if (asciiQueryChars.get(c) && !(encodePercent && c == '%')) {
|
||||
escape = false;
|
||||
}
|
||||
} else if (!Character.isISOControl(c) && !Character.isSpaceChar(c)) {
|
||||
escape = false;
|
||||
}
|
||||
if (!escape) {
|
||||
if (outBuf != null)
|
||||
outBuf.append(c);
|
||||
} else {
|
||||
if (outBuf == null) {
|
||||
outBuf = new StringBuilder(in.length() + 5 * 3);
|
||||
outBuf.append(in, 0, i);
|
||||
formatter = new Formatter(outBuf);
|
||||
}
|
||||
formatter.format("%%%02X", (int) c);
|
||||
}
|
||||
}
|
||||
return outBuf != null ? outBuf : in;
|
||||
}
|
||||
|
||||
protected static final BitSet asciiQueryChars;
|
||||
static {
|
||||
char[] c_unreserved = "_-!.~'()*".toCharArray();
|
||||
char[] c_punct = ",;:$&+=".toCharArray();
|
||||
char[] c_reserved = "/@".toCharArray();
|
||||
asciiQueryChars = new BitSet(128);
|
||||
for (char c = 'a'; c <= 'z'; c++)
|
||||
asciiQueryChars.set(c);
|
||||
for (char c = 'A'; c <= 'Z'; c++)
|
||||
asciiQueryChars.set(c);
|
||||
for (char c = '0'; c <= '9'; c++)
|
||||
asciiQueryChars.set(c);
|
||||
for (char c : c_unreserved)
|
||||
asciiQueryChars.set(c);
|
||||
for (char c : c_punct)
|
||||
asciiQueryChars.set(c);
|
||||
for (char c : c_reserved)
|
||||
asciiQueryChars.set(c);
|
||||
asciiQueryChars.set('%');
|
||||
}
|
||||
|
||||
private void assignRelayDataToInbound(Properties prop, Header[] responseHeaders, int status) {
|
||||
Properties headerProp = new Properties();
|
||||
if (responseHeaders != null) {
|
||||
for (Header header : responseHeaders) {
|
||||
String name = header.getName();
|
||||
String value = header.getValue();
|
||||
String existing = headerProp.getProperty(name);
|
||||
if (existing != null) {
|
||||
// 동일 이름의 헤더가 여러 개인 경우 콤마로 합침 (RFC 7230)
|
||||
headerProp.put(name, existing + ", " + value);
|
||||
} else {
|
||||
headerProp.put(name, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
headerProp.put(HttpAdapterServiceBypass.HTTP_STATUS, String.valueOf(status));
|
||||
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
map.put(HttpAdapterServiceKey.OUTBOUND_RESPONSE_HEADERS, headerProp);
|
||||
|
||||
prop.put(HttpAdapterServiceKey.OUTBOUND_PROPERTY_MAP, map);
|
||||
}
|
||||
|
||||
private void setAuthHeaders(HttpUriRequestBase method, OAuth2AccessTokenVO accessToken) throws Exception {
|
||||
method.setHeader("Authorization",
|
||||
String.format("%s %s", AccessTokenVO.BEARER_TYPE, accessToken.getAccessToken()));
|
||||
}
|
||||
|
||||
private boolean checkTokenRetry(byte[] responseMessage, String tokenErrorCodeKey, String tokenErrorCodeValues,
|
||||
String encode) {
|
||||
if (responseMessage == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(tokenErrorCodeKey)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(tokenErrorCodeValues)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
DocumentContext jsonContext = JsonPath.parse(new String(responseMessage, encode));
|
||||
String responseCode = jsonContext.read(tokenErrorCodeKey);
|
||||
if (StringUtils.isBlank(responseCode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String[] arr = org.springframework.util.StringUtils.tokenizeToStringArray(tokenErrorCodeValues, ",");
|
||||
return ArrayUtils.contains(arr, responseCode);
|
||||
} catch (Exception e) {
|
||||
logger.error("HttpClient5AdapterServiceBypass] checkTokenRetry error=" + e.getMessage());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void assignRequestHeaders(HttpUriRequestBase method, Properties headerProp, Properties inProp) {
|
||||
if (headerProp == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Entry<Object, Object> e : headerProp.entrySet()) {
|
||||
String key = (String) e.getKey();
|
||||
String value = (String) e.getValue();
|
||||
|
||||
if (StringUtils.equalsAnyIgnoreCase(key, HttpAdapterServiceBypass.HOP_BY_HOP_HEADERS)
|
||||
|| StringUtils.equalsIgnoreCase(key, HttpHeaders.CONTENT_LENGTH)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (doHandleCompression && StringUtils.equalsIgnoreCase(key, HttpHeaders.ACCEPT_ENCODING)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
method.addHeader(key, value);
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Request Header :" + key + "=[" + value + "]");
|
||||
}
|
||||
}
|
||||
|
||||
if (doForwardIP) {
|
||||
String forHeaderName = "X-Forwarded-For";
|
||||
String forHeader = inProp.getProperty(HttpAdapterServiceKey.INBOUND_REMOTE_ADDR);
|
||||
if (StringUtils.isNotBlank(forHeader)) {
|
||||
String existingForHeader = headerProp.getProperty(forHeaderName);
|
||||
if (existingForHeader != null) {
|
||||
forHeader = existingForHeader + ", " + forHeader;
|
||||
}
|
||||
method.addHeader(forHeaderName, forHeader);
|
||||
}
|
||||
|
||||
String protoHeaderName = "X-Forwarded-Proto";
|
||||
String protoHeader = inProp.getProperty(HttpAdapterServiceKey.INBOUND_SCHEME);
|
||||
if (StringUtils.isNotBlank(protoHeader)) {
|
||||
method.addHeader(protoHeaderName, protoHeader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private JSONObject parseJson(String message) throws Exception {
|
||||
if (message == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (JSONObject) JSONValue.parse(message);
|
||||
}
|
||||
|
||||
public static final String getIgnoreCaseProp(Properties prop, String key) {
|
||||
if (prop == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (Entry<Object, Object> e : prop.entrySet()) {
|
||||
if (StringUtils.equalsIgnoreCase(key, (String) e.getKey())) {
|
||||
return (String) e.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+772
-505
File diff suppressed because it is too large
Load Diff
-150
@@ -1,150 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.dom4j.Node;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey;
|
||||
import com.eactive.eai.adapter.http.client.impl.filter.HttpClient5AdapterFilterFactory;
|
||||
import com.eactive.eai.adapter.http.client.impl.filter.HttpClientAdapterFilter;
|
||||
import com.eactive.eai.common.util.JacksonUtil;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
/**
|
||||
* 1. 기능 : HttpClient5AdapterServiceRest 호출 전후 Filter 적용할 수 있는 기능을 제공한다.<br>
|
||||
* 2. 처리 개요 : <br>
|
||||
* 3. 주의사항 <br>
|
||||
*
|
||||
* @author :
|
||||
* @version : v 1.0.0
|
||||
* @see : HttpClientAdapterServiceFactory.java,
|
||||
* HttpClientAdapterServiceSupport.java, HttpClient5AdapterServiceRest.java
|
||||
* @since :
|
||||
*
|
||||
*/
|
||||
public class HttpClient5AdapterServiceRestAddFilter extends HttpClient5AdapterServiceRest
|
||||
implements HttpClientAdapterServiceKey {
|
||||
|
||||
// 요청전 수행할 필터(쉼표(,)로 구분)
|
||||
static final String PRE_FILTERS = "PRE_FILTERS";
|
||||
|
||||
// 요청후 수행할 필터(쉼표(,)로 구분)
|
||||
static final String POST_FILTERS = "POST_FILTERS";
|
||||
|
||||
// // Adapter에서 Exception이 발생한 경우, 처리할 필터
|
||||
// static final String EXCEPTION_FILTER = "EXCEPTION_FILTER";
|
||||
|
||||
// 기본 ObjectMapper 는 JsonNode 직렬화 시 작은 소수를 1.2E-7 로 바꿔버린다.
|
||||
private ObjectMapper mapper = JacksonUtil.newNumberSafeMapper();
|
||||
|
||||
/**
|
||||
* 1. 기능 : HttpClient 호출 전후 Filter 적용용 2. 처리 개요 : <br>
|
||||
* 3. 주의사항 <br>
|
||||
*
|
||||
* @param prop Http Adapter 속성 정보
|
||||
* @return 반환 된 Object
|
||||
* @exception Exception 수동 시스템 간 통신 중 발생
|
||||
*/
|
||||
public Object execute(Properties prop, Object data, Properties tempProp) throws Exception {
|
||||
|
||||
String adptGrpName = tempProp.getProperty(ADAPTER_GROUP_NAME);
|
||||
String adptName = tempProp.getProperty(ADAPTER_NAME);
|
||||
|
||||
data = doPreFilters(adptGrpName, adptName, prop, data, tempProp);
|
||||
|
||||
Object adapterResponse = super.execute(prop, data, tempProp);
|
||||
|
||||
adapterResponse = doPostFilters(adptGrpName, adptName, prop, adapterResponse, tempProp);
|
||||
if (adapterResponse instanceof JSONObject)
|
||||
adapterResponse = ((JSONObject) adapterResponse).toJSONString();
|
||||
else if (adapterResponse instanceof ObjectNode)
|
||||
adapterResponse = mapper.writeValueAsString((ObjectNode) adapterResponse);
|
||||
else if (adapterResponse instanceof Node)
|
||||
adapterResponse = ((Node) adapterResponse).asXML();
|
||||
|
||||
return adapterResponse;
|
||||
}
|
||||
|
||||
protected Object doPreFilters(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
|
||||
// boolean isSetCommonFilterInAdapterProp = false;
|
||||
// 1. adapter에 설정된 필터 수행
|
||||
String preFiltersStr = prop.getProperty(PRE_FILTERS);
|
||||
if (StringUtils.isNotEmpty(preFiltersStr)) {
|
||||
String[] preFilters = StringUtils.split(preFiltersStr, ",");
|
||||
for (String filterName : preFilters) {
|
||||
if (StringUtils.isBlank(filterName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.debug("HttpClient5AdapterServiceRestAddFilter] Processing Start [" + filterName + "]");
|
||||
HttpClientAdapterFilter adapterFilter = HttpClient5AdapterFilterFactory.createFilter(filterName.trim());
|
||||
|
||||
if (adapterFilter != null) {
|
||||
// if (adapterFilter instanceof IBKOutCommonFilter)
|
||||
// isSetCommonFilterInAdapterProp = true;
|
||||
|
||||
message = adapterFilter.doPreFilter(adptGrpName, adptName, prop, message, tempProp);
|
||||
} else {
|
||||
throw new Exception("Failed get Filter Class: " + filterName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// // 2. IBKOutCommonFilter 필터 수행(adapter 필터에서 수행하지 않을 때만)
|
||||
// if (!isSetCommonFilterInAdapterProp) {
|
||||
// HttpClientAdapterFilter ibkFilter = HttpClient5AdapterFilterFactory
|
||||
// .createFilter(IBKOutCommonFilter.class.getName());
|
||||
// if (ibkFilter != null) {
|
||||
// message = ibkFilter.doPreFilter(adptGrpName, adptName, prop, message, tempProp);
|
||||
// }
|
||||
// } else {
|
||||
// logger.debug("IBKOutCommonFilter isSetCommonFilterInAdapterProp 'POST_FILTERS'. already in adapter filters");
|
||||
// }
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
protected Object doPostFilters(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
|
||||
// boolean isSetCommonFilterInAdapterProp = false;
|
||||
// 1. adapter에 설정된 필터 수행
|
||||
String postFiltersStr = prop.getProperty(POST_FILTERS);
|
||||
if (StringUtils.isNotEmpty(postFiltersStr)) {
|
||||
String[] postFilters = StringUtils.split(postFiltersStr, ",");
|
||||
for (String filterName : postFilters) {
|
||||
if (StringUtils.isBlank(filterName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
HttpClientAdapterFilter adapterFilter = HttpClient5AdapterFilterFactory.createFilter(filterName.trim());
|
||||
if (adapterFilter != null) {
|
||||
// if (adapterFilter instanceof IBKOutCommonFilter)
|
||||
// isSetCommonFilterInAdapterProp = true;
|
||||
message = adapterFilter.doPostFilter(adptGrpName, adptName, prop, message, tempProp);
|
||||
} else {
|
||||
throw new Exception("Failed get Filter Class: " + filterName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// // 2. IBKOutCommonFilter 필터 수행
|
||||
// if (!isSetCommonFilterInAdapterProp) {
|
||||
// HttpClientAdapterFilter ibkFilter = HttpClient5AdapterFilterFactory
|
||||
// .createFilter(IBKOutCommonFilter.class.getName());
|
||||
// if (ibkFilter != null) {
|
||||
// message = ibkFilter.doPostFilter(adptGrpName, adptName, prop, message, tempProp);
|
||||
// }
|
||||
// } else {
|
||||
// logger.debug("IBKOutCommonFilter isSetCommonFilterInAdapterProp 'PRE_FILTERS'. already in adapter filters");
|
||||
// }
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -65,7 +65,7 @@ public class RetryPolicy {
|
||||
|
||||
return new RetryPolicy(getInt(props, RETRY_MAX_RETRIES, 0), getLong(props, RETRY_BACKOFF_MS, 1000L), codes,
|
||||
getBoolean(props, RETRY_ON_CONNECT_FAIL, false), getBoolean(props, RETRY_ON_TIMEOUT, false),
|
||||
getBoolean(props, RETRY_IDEMPOTENT_ONLY, false));
|
||||
getBoolean(props, RETRY_IDEMPOTENT_ONLY, true));
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class AllHeaderFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String PROPERTIES_GROUP_NAME = "HttpHeaderFilter";
|
||||
public static final String HEADER_KEY_NAMES = "AllHeaderFilter.blackList";
|
||||
|
||||
|
||||
String[] HttpHeaderRelayBlackList = {
|
||||
"Content-Length",
|
||||
"Transfer-Encoding",
|
||||
"Host",
|
||||
"Authorization",
|
||||
"Accept",
|
||||
"Host",
|
||||
"Cookie",
|
||||
"Connection",
|
||||
"Keep-Alive",
|
||||
"Proxy-Authenticate",
|
||||
"Proxy-Authorization",
|
||||
"TE",
|
||||
"Trailer",
|
||||
"Transfer-Encoding",
|
||||
"Upgrade",
|
||||
"Content-Type",
|
||||
"Content-Encoding",
|
||||
"Content-Language",
|
||||
"Content-Location",
|
||||
"Content-MD5",
|
||||
"Expect",
|
||||
};
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
logger.debug("doPreFilter Processing Start.");
|
||||
|
||||
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
|
||||
if (filterHeaders == null) {
|
||||
filterHeaders = new Properties();
|
||||
tempProp.put("FILTERHEADER", filterHeaders);
|
||||
}
|
||||
|
||||
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
|
||||
|
||||
String propValue = PropManager.getInstance().getProperty(PROPERTIES_GROUP_NAME, HEADER_KEY_NAMES, "").trim();
|
||||
|
||||
String[] userSettingBlackList = propValue.split(",");
|
||||
|
||||
if( userSettingBlackList.length > 0 ) {
|
||||
HttpHeaderRelayBlackList = Stream
|
||||
.concat(Arrays.stream(HttpHeaderRelayBlackList), Arrays.stream(userSettingBlackList))
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
|
||||
for(String keyName : inboundHeaderMap.keySet() ) {
|
||||
|
||||
if( StringUtils.equalsAnyIgnoreCase( keyName, HttpHeaderRelayBlackList ) ) {
|
||||
logger.debug("Skip Processing Key ["+keyName+"], value ["+inboundHeaderMap.get(keyName)+"] in HttpHeaderRelayBlackList");
|
||||
continue;
|
||||
}
|
||||
|
||||
filterHeaders.put(keyName, inboundHeaderMap.get(keyName));
|
||||
logger.debug("Processing Key ["+keyName+"], value ["+inboundHeaderMap.get(keyName)+"]");
|
||||
}
|
||||
|
||||
logger.debug("doPreFilter Processing End.");
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public Map<String, String> getInboundHeaderProp(Properties prop) {
|
||||
Properties header = new Properties();
|
||||
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
try {
|
||||
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
if (headerObj instanceof Properties) { //tomcat
|
||||
header = (Properties) headerObj;
|
||||
for (String name : header.stringPropertyNames()) {
|
||||
inboundHeaderMap.put(name, header.getProperty(name));
|
||||
}
|
||||
} else if (headerObj instanceof String) { //weblogic
|
||||
String str = (String) headerObj;
|
||||
str = str.substring(1, str.length() - 1);
|
||||
// key=value 쌍 분리
|
||||
String[] entries = str.split(",");
|
||||
for (String entry : entries) {
|
||||
String[] kv = entry.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return inboundHeaderMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
-118
@@ -1,118 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
|
||||
public class AsyncReponseFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String X_TRACE_ID = "partnerTraceId";
|
||||
public static final String TRACE_ID = "traceId";
|
||||
|
||||
public static final String PROPERTIES_GROUP_NAME = "HttpHeaderFilter";
|
||||
public static final String HEADER_KEY_NAMES = "AsyncReponseFilter";
|
||||
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
logger.debug("AsyncReponseFilter] PreFilter Processing Start!!");
|
||||
String traceId = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID, "");
|
||||
|
||||
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
|
||||
if (filterHeaders == null) {
|
||||
filterHeaders = new Properties();
|
||||
tempProp.put("FILTERHEADER", filterHeaders);
|
||||
}
|
||||
|
||||
String propValue = PropManager.getInstance().getProperty(PROPERTIES_GROUP_NAME, HEADER_KEY_NAMES, "").trim();
|
||||
|
||||
String[] headerKeyNames = propValue.split(",");
|
||||
|
||||
|
||||
headerKeyNames = Arrays.stream(headerKeyNames)
|
||||
.map(String::trim)
|
||||
.toArray(String[]::new);
|
||||
|
||||
try {
|
||||
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
|
||||
|
||||
if (!inboundHeaderMap.isEmpty()) {
|
||||
if (inboundHeaderMap.containsKey(X_TRACE_ID)) {
|
||||
filterHeaders.put(X_TRACE_ID, inboundHeaderMap.get(X_TRACE_ID));
|
||||
logger.debug("AsyncReponseFilter] Processing Key ["+X_TRACE_ID+"], value ["+inboundHeaderMap.get(X_TRACE_ID)+"]");
|
||||
}
|
||||
|
||||
if (inboundHeaderMap.containsKey(TransactionContextKeys.X_LOAN_TOKEN)) {
|
||||
filterHeaders.put(TransactionContextKeys.X_LOAN_TOKEN, inboundHeaderMap.get(TransactionContextKeys.X_LOAN_TOKEN));
|
||||
logger.debug("AsyncReponseFilter] Processing Key ["+TransactionContextKeys.X_LOAN_TOKEN+"], value ["+inboundHeaderMap.get(TransactionContextKeys.X_LOAN_TOKEN)+"]");
|
||||
}
|
||||
}
|
||||
|
||||
filterHeaders.setProperty(TRACE_ID, traceId);
|
||||
logger.debug("AsyncReponseFilter] Processing Key ["+TRACE_ID+"], value ["+traceId+"]");
|
||||
|
||||
for (String keyName : inboundHeaderMap.keySet()) {
|
||||
|
||||
if (StringUtils.equalsAnyIgnoreCase(keyName, headerKeyNames)) {
|
||||
String value = inboundHeaderMap.get(keyName);
|
||||
filterHeaders.put(keyName, value);
|
||||
logger.debug("AsyncReponseFilter] Processing Key [" + keyName + "], value [" + value + "]");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public Map<String, String> getInboundHeaderProp(Properties prop) {
|
||||
Properties header = new Properties();
|
||||
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
try {
|
||||
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
if (headerObj instanceof Properties) { //tomcat
|
||||
header = (Properties) headerObj;
|
||||
for (String name : header.stringPropertyNames()) {
|
||||
inboundHeaderMap.put(name, header.getProperty(name));
|
||||
}
|
||||
} else if (headerObj instanceof String) { //weblogic
|
||||
String str = (String) headerObj;
|
||||
str = str.substring(1, str.length() - 1);
|
||||
// key=value 쌍 분리
|
||||
String[] entries = str.split(",");
|
||||
for (String entry : entries) {
|
||||
String[] kv = entry.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return inboundHeaderMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class DebugLoggerOutFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
logger.debug("Outbound Filter SND adptGrpName : {}", adptGrpName);
|
||||
logger.debug("Outbound Filter SND adptName : {}", adptName);
|
||||
logger.debug("Outbound Filter SND prop : {}", prop);
|
||||
logger.debug("Outbound Filter SND message : {}", message);
|
||||
logger.debug("Outbound Filter SND tempProp : {}", tempProp);
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
logger.debug("Outbound Filter RCV adptGrpName : {}", adptGrpName);
|
||||
logger.debug("Outbound Filter RCV adptName : {}", adptName);
|
||||
logger.debug("Outbound Filter RCV prop : {}", prop);
|
||||
logger.debug("Outbound Filter RCV message : {}", message);
|
||||
logger.debug("Outbound Filter RCV tempProp : {}", tempProp);
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
-90
@@ -1,90 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class HttpClient5AdapterFilterFactory {
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
static String customBasePackage = "com.eactive.eai.custom.adapter.http.client.impl.filter";
|
||||
static String basePackage = "com.eactive.eai.adapter.http.client.impl.filter";
|
||||
private static ConcurrentHashMap<String, HttpClientAdapterFilter> h = new ConcurrentHashMap<>();
|
||||
|
||||
private HttpClient5AdapterFilterFactory() {
|
||||
throw new IllegalStateException("Utility class");
|
||||
}
|
||||
|
||||
public static HttpClientAdapterFilter createFilter(String type) {
|
||||
if (h.containsKey(type)) {
|
||||
return h.get(type);
|
||||
}
|
||||
|
||||
HttpClientAdapterFilter filter = null;
|
||||
switch (HttpClient5AdapterFilterType.getValue(type)) {
|
||||
case SIMPLEFRAMEWORK:
|
||||
filter = new SimpleFrameworkFilter();
|
||||
break;
|
||||
case SIMPLEFRAMEWORKBODY:
|
||||
filter = new SimpleframeworkBodyFilter();
|
||||
break;
|
||||
case JBOBP:
|
||||
filter = new JBOBPFilter();
|
||||
break;
|
||||
case KFTCFACE:
|
||||
filter = new KFTCFaceFilter();
|
||||
break;
|
||||
case KFTCP2P:
|
||||
filter = new KFTCP2PFilter();
|
||||
break;
|
||||
case KAKAOBANK:
|
||||
filter = new KAKAOBankFilter();
|
||||
break;
|
||||
case NAVERFIN:
|
||||
filter = new NAVERFinFilter();
|
||||
break;
|
||||
case NICECREDIT:
|
||||
filter = new NICECreditFilter();
|
||||
break;
|
||||
case TRACEBODY:
|
||||
filter = new TraceBodyFilter();
|
||||
break;
|
||||
case TOSSBANK:
|
||||
filter = new TOSSBankFilter();
|
||||
break;
|
||||
case ASYNCRESPONSE:
|
||||
filter = new AsyncReponseFilter();
|
||||
break;
|
||||
case ALLHEADER:
|
||||
filter = new AllHeaderFilter();
|
||||
break;
|
||||
default:
|
||||
filter = classForName(type);
|
||||
if (filter == null) {
|
||||
filter = classForName(customBasePackage + "." + type);
|
||||
}
|
||||
if (filter == null) {
|
||||
filter = classForName(basePackage + "." + type);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (filter != null) {
|
||||
h.put(type, filter);
|
||||
return filter;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpClientAdapterFilter classForName(String type) {
|
||||
try {
|
||||
Class<?> cl = Class.forName(type);
|
||||
return (HttpClientAdapterFilter) cl.newInstance();
|
||||
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
|
||||
logger.error("Cannot create a HttpClientAdapterFilter. - {}", type);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
public enum HttpClient5AdapterFilterType {
|
||||
SIMPLEFRAMEWORK,
|
||||
SIMPLEFRAMEWORKBODY,
|
||||
JBOBP,
|
||||
KFTCFACE,
|
||||
KFTCP2P,
|
||||
KAKAOBANK,
|
||||
NAVERFIN,
|
||||
NICECREDIT,
|
||||
TOSSBANK,
|
||||
TRACEBODY,
|
||||
ASYNCRESPONSE,
|
||||
ALLHEADER,
|
||||
UNKNOWN;
|
||||
|
||||
public static HttpClient5AdapterFilterType getValue(String type) {
|
||||
try {
|
||||
return valueOf(type.toUpperCase());
|
||||
} catch (NullPointerException e) {
|
||||
return UNKNOWN;
|
||||
} catch (Exception e) {
|
||||
return UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
public interface HttpClientAdapterFilter {
|
||||
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception;
|
||||
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception;
|
||||
}
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public class HttpClientAdapterFilterException extends RuntimeException {
|
||||
private static final long serialVersionUID = -1208983360831093751L;
|
||||
@Getter
|
||||
private final String code;
|
||||
@Getter
|
||||
private final Properties prop;
|
||||
@Getter
|
||||
private final Properties tempProp;
|
||||
|
||||
public HttpClientAdapterFilterException(String code, String msg, Properties prop, Properties tmepProp) {
|
||||
super(msg);
|
||||
this.code = code;
|
||||
this.prop = prop;
|
||||
this.tempProp = tmepProp;
|
||||
}
|
||||
|
||||
public HttpClientAdapterFilterException(String code, String msg, Properties prop, Properties tempProp, Throwable cause) {
|
||||
super(msg, cause);
|
||||
this.code = code;
|
||||
this.prop = prop;
|
||||
this.tempProp = tempProp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HttpClientAdapterFilterException [code=" + code + ", prop=" + prop + ", tempProp=" + tempProp + ", parent=" + super.toString() + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.property.PropGroupVO;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
|
||||
public class JBOBPFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String KJB_HEADER = "KJB_Header";
|
||||
public static final String X_OBP_PARTNERCODE = "x-obp-partnercode";
|
||||
public static final String X_OBP_TXID = "x-obp-txid";
|
||||
public static final String X_OBP_TRUST_SYSTEM = "x-obp-trust-system";
|
||||
String systemTrustKey = "APIMGW-0001-QVBJTUdXLTAwMDE=";
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
logger.debug("JBOBPFilter] PreFilter Processing Start!!");
|
||||
PropGroupVO propGroupVo = PropManager.getInstance().getPropGroupVO(KJB_HEADER);
|
||||
if (propGroupVo != null) systemTrustKey = propGroupVo.getProperty(X_OBP_TRUST_SYSTEM, systemTrustKey);
|
||||
|
||||
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
|
||||
if (filterHeaders == null) {
|
||||
filterHeaders = new Properties();
|
||||
tempProp.put("FILTERHEADER", filterHeaders);
|
||||
}
|
||||
try {
|
||||
String obpPartnerCode = "000000-00";
|
||||
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
|
||||
|
||||
if (!inboundHeaderMap.isEmpty()) {
|
||||
if (inboundHeaderMap.containsKey(X_OBP_PARTNERCODE)) {
|
||||
filterHeaders.put(X_OBP_PARTNERCODE, inboundHeaderMap.get(X_OBP_PARTNERCODE));
|
||||
logger.debug("JBOBPFilter] Processing Key ["+X_OBP_PARTNERCODE+"], value ["+inboundHeaderMap.get(X_OBP_PARTNERCODE)+"]");
|
||||
} else {
|
||||
filterHeaders.put(X_OBP_PARTNERCODE, obpPartnerCode);
|
||||
logger.debug("JBOBPFilter] Processing Key ["+X_OBP_PARTNERCODE+"], value ["+obpPartnerCode+"]");
|
||||
}
|
||||
} else {
|
||||
filterHeaders.put(X_OBP_PARTNERCODE, obpPartnerCode);
|
||||
logger.debug("JBOBPFilter] Processing Key ["+X_OBP_PARTNERCODE+"], value ["+obpPartnerCode+"]");
|
||||
}
|
||||
|
||||
String uuid = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID, "");
|
||||
if (StringUtils.isNotBlank(uuid)) {
|
||||
filterHeaders.setProperty(X_OBP_TXID, uuid); // JB OBP Framework용
|
||||
logger.debug("JBOBPFilter] Processing Key ["+X_OBP_TXID+"], value ["+uuid+"]");
|
||||
}
|
||||
if (StringUtils.isNotBlank(systemTrustKey)) {
|
||||
filterHeaders.setProperty(X_OBP_TRUST_SYSTEM, systemTrustKey); // JB OBP Framework용
|
||||
logger.debug("JBOBPFilter] Processing Key ["+X_OBP_TRUST_SYSTEM+"], value ["+systemTrustKey+"]");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public Map<String, String> getInboundHeaderProp(Properties prop) {
|
||||
Properties header = new Properties();
|
||||
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
try {
|
||||
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
if (headerObj instanceof Properties) { //tomcat
|
||||
header = (Properties) headerObj;
|
||||
for (String name : header.stringPropertyNames()) {
|
||||
inboundHeaderMap.put(name, header.getProperty(name));
|
||||
}
|
||||
} else if (headerObj instanceof String) { //weblogic
|
||||
String str = (String) headerObj;
|
||||
str = str.substring(1, str.length() - 1);
|
||||
// key=value 쌍 분리
|
||||
String[] entries = str.split(",");
|
||||
for (String entry : entries) {
|
||||
String[] kv = entry.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return inboundHeaderMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.property.PropGroupVO;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
|
||||
public class KAKAOBankFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String KJB_HEADER = "KJB_Header";
|
||||
public static final String PROP_PREFIX = "kakaobank";
|
||||
public static final String X_KKB_PARTNER_CODE = "X-KKB-PARTNER-CODE";
|
||||
public static final String X_KKB_API_NAME = "X-KKB-API-NAME";
|
||||
public static final String X_KKB_API_TX_ID = "X-KKB-API-TX-ID";
|
||||
public static final String X_KKB_TX_TIME = "X-KKB-TX-TIME";
|
||||
public static final String X_KKB_CMPR_MGMT_NO = "X-KKB-CMPR-MGMT-NO";
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
logger.debug("KAKAOBankFilter] PreFilter Processing Start!!");
|
||||
Object returnMessage = message;
|
||||
String partnerCode = "KJB";
|
||||
String apiName = "status_change";
|
||||
String apiTxId = "0";
|
||||
String txTime = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
|
||||
String cmprMgmtNo = "0";
|
||||
|
||||
PropGroupVO propGroupVo = PropManager.getInstance().getPropGroupVO(KJB_HEADER);
|
||||
if (propGroupVo != null) {
|
||||
partnerCode = propGroupVo.getProperty(PROP_PREFIX + "." + X_KKB_PARTNER_CODE);
|
||||
apiName = propGroupVo.getProperty(PROP_PREFIX + "." + X_KKB_API_NAME);
|
||||
apiTxId = propGroupVo.getProperty(PROP_PREFIX + "." + X_KKB_API_TX_ID);
|
||||
cmprMgmtNo = propGroupVo.getProperty(PROP_PREFIX + "." + X_KKB_CMPR_MGMT_NO);
|
||||
}
|
||||
|
||||
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
|
||||
if (filterHeaders == null) {
|
||||
filterHeaders = new Properties();
|
||||
tempProp.put("FILTERHEADER", filterHeaders);
|
||||
}
|
||||
try {
|
||||
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
|
||||
|
||||
if (!inboundHeaderMap.isEmpty()) {
|
||||
if (inboundHeaderMap.containsKey(X_KKB_PARTNER_CODE)) {
|
||||
filterHeaders.put(X_KKB_PARTNER_CODE, inboundHeaderMap.get(X_KKB_PARTNER_CODE));
|
||||
} else {
|
||||
filterHeaders.put(X_KKB_PARTNER_CODE, partnerCode);
|
||||
}
|
||||
if (inboundHeaderMap.containsKey(X_KKB_API_NAME)) {
|
||||
filterHeaders.put(X_KKB_API_NAME, inboundHeaderMap.get(X_KKB_API_NAME));
|
||||
} else {
|
||||
filterHeaders.put(X_KKB_API_NAME, apiName);
|
||||
}
|
||||
if (inboundHeaderMap.containsKey(X_KKB_API_TX_ID)) {
|
||||
filterHeaders.put(X_KKB_API_TX_ID, inboundHeaderMap.get(X_KKB_API_TX_ID));
|
||||
} else {
|
||||
filterHeaders.put(X_KKB_API_TX_ID, apiTxId);
|
||||
}
|
||||
if (inboundHeaderMap.containsKey(X_KKB_TX_TIME)) {
|
||||
filterHeaders.put(X_KKB_TX_TIME, inboundHeaderMap.get(X_KKB_TX_TIME));
|
||||
} else {
|
||||
filterHeaders.put(X_KKB_TX_TIME, txTime);
|
||||
}
|
||||
if (inboundHeaderMap.containsKey(X_KKB_CMPR_MGMT_NO)) {
|
||||
filterHeaders.put(X_KKB_CMPR_MGMT_NO, inboundHeaderMap.get(X_KKB_CMPR_MGMT_NO));
|
||||
} else {
|
||||
filterHeaders.put(X_KKB_CMPR_MGMT_NO, cmprMgmtNo);
|
||||
}
|
||||
} else {
|
||||
filterHeaders.put(X_KKB_PARTNER_CODE, partnerCode);
|
||||
filterHeaders.put(X_KKB_API_NAME, apiName);
|
||||
filterHeaders.put(X_KKB_API_TX_ID, apiTxId);
|
||||
filterHeaders.put(X_KKB_TX_TIME, txTime);
|
||||
filterHeaders.put(X_KKB_CMPR_MGMT_NO, cmprMgmtNo);
|
||||
}
|
||||
logger.debug("KAKAOBankFilter] Processing Key ["+X_KKB_PARTNER_CODE+"], value ["+filterHeaders.getProperty(X_KKB_PARTNER_CODE)+"]");
|
||||
logger.debug("KAKAOBankFilter] Processing Key ["+X_KKB_API_NAME+"], value ["+filterHeaders.getProperty(X_KKB_API_NAME)+"]");
|
||||
logger.debug("KAKAOBankFilter] Processing Key ["+X_KKB_API_TX_ID+"], value ["+filterHeaders.getProperty(X_KKB_API_TX_ID)+"]");
|
||||
logger.debug("KAKAOBankFilter] Processing Key ["+X_KKB_TX_TIME+"], value ["+filterHeaders.getProperty(X_KKB_TX_TIME)+"]");
|
||||
logger.debug("KAKAOBankFilter] Processing Key ["+X_KKB_CMPR_MGMT_NO+"], value ["+filterHeaders.getProperty(X_KKB_CMPR_MGMT_NO)+"]");
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
return returnMessage;
|
||||
}
|
||||
|
||||
public Map<String, String> getInboundHeaderProp(Properties prop) {
|
||||
Properties header = new Properties();
|
||||
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
try {
|
||||
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
if (headerObj instanceof Properties) { //tomcat
|
||||
header = (Properties) headerObj;
|
||||
for (String name : header.stringPropertyNames()) {
|
||||
inboundHeaderMap.put(name, header.getProperty(name));
|
||||
}
|
||||
} else if (headerObj instanceof String) { //weblogic
|
||||
String str = (String) headerObj;
|
||||
str = str.substring(1, str.length() - 1);
|
||||
// key=value 쌍 분리
|
||||
String[] entries = str.split(",");
|
||||
for (String entry : entries) {
|
||||
String[] kv = entry.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return inboundHeaderMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
|
||||
import com.eactive.eai.adapter.AdapterGroupVO;
|
||||
import com.eactive.eai.adapter.AdapterManager;
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.property.PropGroupVO;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.server.EAIServerManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
|
||||
public class KFTCFaceFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String KJB_HEADER = "KJB_Header";
|
||||
public static final String PROP_PREFIX = "kftcface";
|
||||
public static final String H_CLIENT_ID = "Client-Id";
|
||||
public static final String B_ORG_CODE = "org_code";
|
||||
public static final String B_TRANSACTION_ID = "transaction_id";
|
||||
public static final String B_REQUEST_DATETIME = "request_datetime";
|
||||
public static String instId = null;
|
||||
|
||||
static {
|
||||
EAIServerManager eaiServerManager = EAIServerManager.getInstance();
|
||||
instId = eaiServerManager.getInstId();
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
logger.debug("KFTCFaceFilter] PreFilter Processing Start!!");
|
||||
JSONObject returnMessage = null;
|
||||
String clientId = "";
|
||||
String orgCode = "034"; // 광주은행 행코드
|
||||
|
||||
PropGroupVO propGroupVo = PropManager.getInstance().getPropGroupVO(KJB_HEADER);
|
||||
if (propGroupVo != null) {
|
||||
if (StringUtils.isEmpty(clientId))
|
||||
clientId = propGroupVo.getProperty(PROP_PREFIX + "." + H_CLIENT_ID);
|
||||
orgCode = propGroupVo.getProperty(PROP_PREFIX + "." + B_ORG_CODE);
|
||||
}
|
||||
|
||||
String request_datetime = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
|
||||
String reqDate = new SimpleDateFormat("yyMMdd").format(new Date());
|
||||
String reqTime = new SimpleDateFormat("HHmmss").format(new Date());
|
||||
// 기관코드(3) + 요청일자(6) -- 금결원 표준화 항목, 고정 9자리.
|
||||
// 요청시간(6) + 인스턴스ID(2) + RandomString(3) -- 기관별 생성 거래고유번호(11자리)
|
||||
// 기관코드(3) + 요청일자(6) + 요청시간(6) + 인스턴스ID(2) + RandomString(3) -- 20자리.
|
||||
String transaction_id = orgCode + reqDate + reqTime + instId + RandomStringUtils.random(3, true, true).toUpperCase();
|
||||
|
||||
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
|
||||
if (filterHeaders == null) {
|
||||
filterHeaders = new Properties();
|
||||
tempProp.put("FILTERHEADER", filterHeaders);
|
||||
}
|
||||
try {
|
||||
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
|
||||
|
||||
if (!inboundHeaderMap.isEmpty()) {
|
||||
if (inboundHeaderMap.containsKey(H_CLIENT_ID)) {
|
||||
filterHeaders.put(H_CLIENT_ID, inboundHeaderMap.get(H_CLIENT_ID));
|
||||
logger.debug("KFTCFaceFilter] Processing Key ["+H_CLIENT_ID+"], value ["+inboundHeaderMap.get(H_CLIENT_ID)+"]");
|
||||
} else {
|
||||
filterHeaders.put(H_CLIENT_ID, clientId);
|
||||
logger.debug("KFTCFaceFilter] Processing Key ["+H_CLIENT_ID+"], value ["+clientId+"]");
|
||||
}
|
||||
} else {
|
||||
filterHeaders.setProperty(H_CLIENT_ID, clientId);
|
||||
logger.debug("KFTCFaceFilter] Processing Key ["+H_CLIENT_ID+"], value ["+clientId+"]");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
AdapterManager adapterManager = AdapterManager.getInstance();
|
||||
AdapterGroupVO adptGrpVO = adapterManager.getAdapterGroupVO(adptGrpName);
|
||||
String encode = adptGrpVO.getMessageEncode();
|
||||
if (message == null) {
|
||||
returnMessage = new JSONObject();
|
||||
} else {
|
||||
if (message instanceof JSONObject) {
|
||||
returnMessage = (JSONObject) message;
|
||||
} else if (message instanceof String) {
|
||||
returnMessage = parseJson((String) message);
|
||||
} else if (message instanceof byte[]) {
|
||||
returnMessage = parseJson(new String((byte[]) message, encode));
|
||||
}
|
||||
}
|
||||
|
||||
returnMessage.put(B_ORG_CODE, orgCode);
|
||||
returnMessage.put(B_TRANSACTION_ID, transaction_id);
|
||||
returnMessage.put(B_REQUEST_DATETIME, request_datetime);
|
||||
|
||||
return returnMessage.toJSONString();
|
||||
}
|
||||
|
||||
private JSONObject parseJson(String message) throws Exception {
|
||||
if (message == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (JSONObject) JSONValue.parse(message);
|
||||
}
|
||||
|
||||
public Map<String, String> getInboundHeaderProp(Properties prop) {
|
||||
Properties header = new Properties();
|
||||
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
try {
|
||||
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
if (headerObj instanceof Properties) { //tomcat
|
||||
header = (Properties) headerObj;
|
||||
for (String name : header.stringPropertyNames()) {
|
||||
inboundHeaderMap.put(name, header.getProperty(name));
|
||||
}
|
||||
} else if (headerObj instanceof String) { //weblogic
|
||||
String str = (String) headerObj;
|
||||
str = str.substring(1, str.length() - 1);
|
||||
// key=value 쌍 분리
|
||||
String[] entries = str.split(",");
|
||||
for (String entry : entries) {
|
||||
String[] kv = entry.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return inboundHeaderMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.apache.commons.lang3.RandomStringUtils;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.property.PropGroupVO;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.server.EAIServerManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
|
||||
public class KFTCP2PFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String KJB_HEADER = "KJB_Header";
|
||||
public static final String PROP_PREFIX = "kftcp2p";
|
||||
public static final String H_ORG_CODE = "org_code";
|
||||
public static final String H_TRX_NO = "api_trx_no";
|
||||
public static final String H_TRX_DTM = "api_trx_dtm";
|
||||
public static String instId = null;
|
||||
|
||||
static {
|
||||
EAIServerManager eaiServerManager = EAIServerManager.getInstance();
|
||||
instId = eaiServerManager.getInstId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
logger.debug("KFTCP2PFilter] PreFilter Processing Start!!");
|
||||
String orgCode = "D210400012"; // 광주은행 개발 기관코드(운영: K210800020)
|
||||
|
||||
PropGroupVO propGroupVo = PropManager.getInstance().getPropGroupVO(KJB_HEADER);
|
||||
if (propGroupVo != null) {
|
||||
orgCode = propGroupVo.getProperty(PROP_PREFIX + "." + H_ORG_CODE);
|
||||
}
|
||||
|
||||
String apiTrxDtm = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
|
||||
String apiTrxTm = new SimpleDateFormat("HHmmss").format(new Date());
|
||||
// 기관코드(10) + 인스턴스ID(2) + 일시(6) + RandomString(2) -- 총 20자리
|
||||
String apiTrxNo = orgCode + instId + apiTrxTm + RandomStringUtils.random(2, true, true).toUpperCase();
|
||||
|
||||
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
|
||||
if (filterHeaders == null) {
|
||||
filterHeaders = new Properties();
|
||||
tempProp.put("FILTERHEADER", filterHeaders);
|
||||
}
|
||||
try {
|
||||
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
|
||||
|
||||
if (!inboundHeaderMap.isEmpty()) {
|
||||
if (inboundHeaderMap.containsKey(H_TRX_NO)) {
|
||||
filterHeaders.put(H_TRX_NO, inboundHeaderMap.get(H_TRX_NO));
|
||||
logger.debug("KFTCP2PFilter] Processing Key ["+H_TRX_NO+"], value ["+inboundHeaderMap.get(H_TRX_NO)+"]");
|
||||
} else {
|
||||
filterHeaders.put(H_TRX_NO, apiTrxNo);
|
||||
logger.debug("KFTCP2PFilter] Processing Key ["+H_TRX_NO+"], value ["+apiTrxNo+"]");
|
||||
}
|
||||
if (inboundHeaderMap.containsKey(H_TRX_DTM)) {
|
||||
filterHeaders.put(H_TRX_DTM, inboundHeaderMap.get(H_TRX_DTM));
|
||||
logger.debug("KFTCP2PFilter] Processing Key ["+H_TRX_DTM+"], value ["+inboundHeaderMap.get(H_TRX_DTM)+"]");
|
||||
} else {
|
||||
filterHeaders.put(H_TRX_DTM, apiTrxDtm);
|
||||
logger.debug("KFTCP2PFilter] Processing Key ["+H_TRX_DTM+"], value ["+apiTrxDtm+"]");
|
||||
}
|
||||
} else {
|
||||
filterHeaders.put(H_TRX_NO, apiTrxNo);
|
||||
filterHeaders.put(H_TRX_DTM, apiTrxDtm);
|
||||
}
|
||||
logger.debug("KFTCP2PFilter] Processing Key ["+H_TRX_NO+"], value ["+filterHeaders.getProperty(H_TRX_NO)+"]");
|
||||
logger.debug("KFTCP2PFilter] Processing Key ["+H_TRX_DTM+"], value ["+filterHeaders.getProperty(H_TRX_DTM)+"]");
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public Map<String, String> getInboundHeaderProp(Properties prop) {
|
||||
Properties header = new Properties();
|
||||
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
try {
|
||||
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
if (headerObj instanceof Properties) { //tomcat
|
||||
header = (Properties) headerObj;
|
||||
for (String name : header.stringPropertyNames()) {
|
||||
inboundHeaderMap.put(name, header.getProperty(name));
|
||||
}
|
||||
} else if (headerObj instanceof String) { //weblogic
|
||||
String str = (String) headerObj;
|
||||
str = str.substring(1, str.length() - 1);
|
||||
// key=value 쌍 분리
|
||||
String[] entries = str.split(",");
|
||||
for (String entry : entries) {
|
||||
String[] kv = entry.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return inboundHeaderMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.property.PropGroupVO;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
|
||||
public class NAVERFinFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String KJB_HEADER = "KJB_Header";
|
||||
public static final String PROP_PREFIX = "naverfin";
|
||||
public static final String X_PARTNER_ID = "X-Partner-Id";
|
||||
public static final String X_FINTECH_ID = "X-Fintech-Id";
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
logger.debug("NAVERFinFilter] PreFilter Processing Start!!");
|
||||
String partnerID = "r0jtqwC9gAW5";
|
||||
String fintechId = "NF";
|
||||
|
||||
PropGroupVO propGroupVo = PropManager.getInstance().getPropGroupVO(KJB_HEADER);
|
||||
if (propGroupVo != null) {
|
||||
partnerID = propGroupVo.getProperty(PROP_PREFIX + "." + X_PARTNER_ID);
|
||||
fintechId = propGroupVo.getProperty(PROP_PREFIX + "." + X_FINTECH_ID);
|
||||
}
|
||||
|
||||
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
|
||||
if (filterHeaders == null) {
|
||||
filterHeaders = new Properties();
|
||||
tempProp.put("FILTERHEADER", filterHeaders);
|
||||
}
|
||||
try {
|
||||
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
|
||||
|
||||
if (!inboundHeaderMap.isEmpty()) {
|
||||
if (inboundHeaderMap.containsKey(X_PARTNER_ID)) {
|
||||
filterHeaders.put(X_PARTNER_ID, inboundHeaderMap.get(X_PARTNER_ID));
|
||||
logger.debug("NAVERFinFilter] Processing Key ["+X_PARTNER_ID+"], value ["+inboundHeaderMap.get(X_PARTNER_ID)+"]");
|
||||
} else {
|
||||
filterHeaders.put(X_PARTNER_ID, partnerID);
|
||||
logger.debug("NAVERFinFilter] Processing Key ["+X_PARTNER_ID+"], value ["+partnerID+"]");
|
||||
}
|
||||
if (inboundHeaderMap.containsKey(X_FINTECH_ID)) {
|
||||
filterHeaders.put(X_FINTECH_ID, inboundHeaderMap.get(X_FINTECH_ID));
|
||||
logger.debug("NAVERFinFilter] Processing Key ["+X_FINTECH_ID+"], value ["+inboundHeaderMap.get(X_FINTECH_ID)+"]");
|
||||
} else {
|
||||
filterHeaders.put(X_FINTECH_ID, fintechId);
|
||||
logger.debug("NAVERFinFilter] Processing Key ["+X_FINTECH_ID+"], value ["+fintechId+"]");
|
||||
}
|
||||
} else {
|
||||
filterHeaders.put(X_PARTNER_ID, partnerID);
|
||||
filterHeaders.put(X_FINTECH_ID, fintechId);
|
||||
}
|
||||
logger.debug("NAVERFinFilter] Processing Key ["+X_PARTNER_ID+"], value ["+filterHeaders.getProperty(X_PARTNER_ID)+"]");
|
||||
logger.debug("NAVERFinFilter] Processing Key ["+X_FINTECH_ID+"], value ["+filterHeaders.getProperty(X_FINTECH_ID)+"]");
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public Map<String, String> getInboundHeaderProp(Properties prop) {
|
||||
Properties header = new Properties();
|
||||
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
try {
|
||||
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
if (headerObj instanceof Properties) { //tomcat
|
||||
header = (Properties) headerObj;
|
||||
for (String name : header.stringPropertyNames()) {
|
||||
inboundHeaderMap.put(name, header.getProperty(name));
|
||||
}
|
||||
} else if (headerObj instanceof String) { //weblogic
|
||||
String str = (String) headerObj;
|
||||
str = str.substring(1, str.length() - 1);
|
||||
// key=value 쌍 분리
|
||||
String[] entries = str.split(",");
|
||||
for (String entry : entries) {
|
||||
String[] kv = entry.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return inboundHeaderMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Map.Entry;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
|
||||
import com.eactive.eai.adapter.AdapterGroupVO;
|
||||
import com.eactive.eai.adapter.AdapterManager;
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.property.PropGroupVO;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
|
||||
public class NICECreditFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String KJB_HEADER = "KJB_Header";
|
||||
public static final String PROP_PREFIX = "nicecredit";
|
||||
public static final String H_PRODUCTID = "ProductID";
|
||||
public static final String B_REQ_DTM = "req_dtm";
|
||||
public static final String B_GOODS_CLS = "goods_cls";
|
||||
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
JSONObject returnMessage = null;
|
||||
String productId = "2303102120";
|
||||
String reqDtm = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
|
||||
String goodsCls = "KJBAPI0001";
|
||||
|
||||
PropGroupVO propGroupVo = PropManager.getInstance().getPropGroupVO(KJB_HEADER);
|
||||
if (propGroupVo != null) {
|
||||
productId = propGroupVo.getProperty(PROP_PREFIX + "." + H_PRODUCTID);
|
||||
goodsCls = propGroupVo.getProperty(PROP_PREFIX + "." + B_GOODS_CLS);
|
||||
}
|
||||
|
||||
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
|
||||
if (filterHeaders == null) {
|
||||
filterHeaders = new Properties();
|
||||
tempProp.put("FILTERHEADER", filterHeaders);
|
||||
}
|
||||
try {
|
||||
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
|
||||
if (inboundHeaderMap != null && inboundHeaderMap.containsKey(H_PRODUCTID)) {
|
||||
for (Entry<String, String> e : inboundHeaderMap.entrySet()) {
|
||||
String key = (String) e.getKey();
|
||||
String value = (String) e.getValue();
|
||||
|
||||
if (StringUtils.equalsIgnoreCase(key, H_PRODUCTID)) {
|
||||
filterHeaders.setProperty(key, value);
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Request Header :" + key + "=[" + value + "]");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filterHeaders.setProperty(H_PRODUCTID, productId);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
AdapterManager adapterManager = AdapterManager.getInstance();
|
||||
AdapterGroupVO adptGrpVO = adapterManager.getAdapterGroupVO(adptGrpName);
|
||||
String encode = adptGrpVO.getMessageEncode();
|
||||
if (message == null) {
|
||||
returnMessage = new JSONObject();
|
||||
} else {
|
||||
if (message instanceof JSONObject) {
|
||||
returnMessage = (JSONObject) message;
|
||||
} else if (message instanceof String) {
|
||||
returnMessage = parseJson((String) message);
|
||||
} else if (message instanceof byte[]) {
|
||||
returnMessage = parseJson(new String((byte[]) message, encode));
|
||||
}
|
||||
}
|
||||
|
||||
returnMessage.put(B_REQ_DTM, reqDtm);
|
||||
returnMessage.put(B_GOODS_CLS, goodsCls);
|
||||
|
||||
return returnMessage.toJSONString();
|
||||
}
|
||||
|
||||
private JSONObject parseJson(String message) throws Exception {
|
||||
if (message == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (JSONObject) JSONValue.parse(message);
|
||||
}
|
||||
|
||||
public Map<String, String> getInboundHeaderProp(Properties prop) {
|
||||
Properties header = new Properties();
|
||||
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
try {
|
||||
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
if (headerObj instanceof Properties) { //tomcat
|
||||
header = (Properties) headerObj;
|
||||
for (String name : header.stringPropertyNames()) {
|
||||
inboundHeaderMap.put(name, header.getProperty(name));
|
||||
}
|
||||
} else if (headerObj instanceof String) { //weblogic
|
||||
String str = (String) headerObj;
|
||||
str = str.substring(1, str.length() - 1);
|
||||
// key=value 쌍 분리
|
||||
String[] entries = str.split(",");
|
||||
for (String entry : entries) {
|
||||
String[] kv = entry.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return inboundHeaderMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.adapter.http.filter.AbstractCryptoFilter;
|
||||
|
||||
/**
|
||||
* HTTP 클라이언트 어댑터용 암복호화 필터 (송신 방향).
|
||||
*
|
||||
* doPreFilter : 외부 시스템으로 보내는 요청 암호화 (CRYPTO_ENC_* 프로퍼티 기반)
|
||||
* doPostFilter : 외부 시스템에서 받은 응답 복호화 (CRYPTO_DEC_* 프로퍼티 기반)
|
||||
*
|
||||
* DYNAMIC 키 컨텍스트가 필요한 경우 {@link #buildRuntimeContext}를 오버라이드한다.
|
||||
* STATIC 키 모듈을 사용하는 경우 빈 Map이 기본값이므로 오버라이드 불필요.
|
||||
*
|
||||
* 프로퍼티 설명은 {@link AbstractCryptoFilter} 참조.
|
||||
* AAD 관련 프로퍼티:
|
||||
* PROP_AAD_HEADER tempProp에서 AAD 값을 조회할 키 이름.
|
||||
* 미설정 또는 해당 키 없으면 aad=null → IV를 AAD 대체값으로 사용.
|
||||
*/
|
||||
public class OutCryptoFilter extends AbstractCryptoFilter implements HttpClientAdapterFilter {
|
||||
|
||||
/**
|
||||
* DYNAMIC 키 도출용 컨텍스트를 구성한다.
|
||||
* STATIC 키 모듈이면 빈 Map({@code new HashMap<>()})을 반환한다.
|
||||
* 서브클래스에서 오버라이드하여 tempProp으로부터 값을 추출할 수 있다.
|
||||
*/
|
||||
protected Map<String, String> buildRuntimeContext(Properties prop, Properties tempProp) {
|
||||
Map<String, String> context = new HashMap<>();
|
||||
for (String key : prop.stringPropertyNames()) {
|
||||
context.put(key, prop.getProperty(key));
|
||||
}
|
||||
|
||||
for (String key : tempProp.stringPropertyNames()) {
|
||||
context.put(key, tempProp.getProperty(key));
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* CRYPTO_AAD_PROP 프로퍼티에 지정된 키로 tempProp에서 AAD 값을 조회한다.
|
||||
* 미설정 또는 값 없으면 null을 반환하여 GCM 기본 동작(IV를 AAD 대체값으로 사용)을 따른다.
|
||||
*/
|
||||
protected byte[] buildAad(Properties prop, Properties tempProp) {
|
||||
if (tempProp == null) {
|
||||
return null;
|
||||
}
|
||||
String value = tempProp.getProperty(PROP_AAD_HEADER);
|
||||
return StringUtils.isBlank(value) ? null : value.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
|
||||
String moduleName = required(prop, PROP_MODULE_NAME);
|
||||
String scope = getProp(prop, PROP_SCOPE, SCOPE_FIELD).toUpperCase();
|
||||
String body = toBodyString(message);
|
||||
Map<String, String> runtimeCtx = buildRuntimeContext(prop, tempProp);
|
||||
byte[] aad = buildAad(prop, tempProp);
|
||||
|
||||
if (SCOPE_FIELD.equals(scope)) {
|
||||
return encryptField(body, moduleName, runtimeCtx, aad,
|
||||
getProp(tempProp, PROP_ENC_FROM_PATH, PATH_ROOT),
|
||||
getProp(tempProp, PROP_ENC_TO_PATH, PATH_ENCDATA));
|
||||
}
|
||||
return encryptBody(body, moduleName, runtimeCtx, aad);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
|
||||
String moduleName = required(prop, PROP_MODULE_NAME);
|
||||
String scope = getProp(prop, PROP_SCOPE, SCOPE_FIELD).toUpperCase();
|
||||
String body = toBodyString(message);
|
||||
Map<String, String> runtimeCtx = buildRuntimeContext(prop, tempProp);
|
||||
byte[] aad = buildAad(prop, tempProp);
|
||||
|
||||
if (SCOPE_FIELD.equals(scope)) {
|
||||
return decryptField(body, moduleName, runtimeCtx, aad,
|
||||
getProp(tempProp, PROP_DEC_FROM_PATH, PATH_ENCDATA),
|
||||
getProp(tempProp, PROP_DEC_TO_PATH, PATH_ROOT));
|
||||
}
|
||||
return decryptBody(body, moduleName, runtimeCtx, aad);
|
||||
}
|
||||
}
|
||||
-109
@@ -1,109 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class SimpleFrameworkFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String CLIENTID = "clientId";
|
||||
public static final String TRACEID = "traceId";
|
||||
public static final String GUID = "guid";
|
||||
public static final String RECEIVEDTIMESTAMP = "receivedTimestamp";
|
||||
public static final String PARTNER_TRACE_ID = "partnerTraceId";
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
logger.debug("SimpleFrameworkFilter] PreFilter Processing Start!!");
|
||||
|
||||
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
|
||||
if (filterHeaders == null) {
|
||||
filterHeaders = new Properties();
|
||||
tempProp.put("FILTERHEADER", filterHeaders);
|
||||
}
|
||||
|
||||
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
|
||||
|
||||
try {
|
||||
/* 업체정보 */
|
||||
String clientId = tempProp.getProperty(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID);
|
||||
if (StringUtils.isEmpty(clientId)) clientId = "";
|
||||
filterHeaders.put(CLIENTID, clientId);
|
||||
logger.debug("SimpleFrameworkFilter] Processing Key ["+CLIENTID+"], value ["+clientId+"]");
|
||||
|
||||
/* APIM 거래추적자 */
|
||||
String uuid = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID, "");
|
||||
filterHeaders.put(TRACEID, uuid);
|
||||
logger.debug("SimpleFrameworkFilter] Processing Key ["+TRACEID+"], value ["+uuid+"]");
|
||||
|
||||
/* GUID */
|
||||
String guid = tempProp.getProperty(TransactionContextKeys.GUID, "");
|
||||
filterHeaders.put(GUID, guid);
|
||||
logger.debug("SimpleFrameworkFilter] Processing Key ["+GUID+"], value ["+guid+"]");
|
||||
|
||||
/* 최초요청시간 */
|
||||
String receivedTimestamp = tempProp.getProperty(HttpAdapterServiceKey.INBOUND_REQUESTED_TIME, "");
|
||||
filterHeaders.put(RECEIVEDTIMESTAMP, receivedTimestamp);
|
||||
logger.debug("SimpleFrameworkFilter] Processing Key ["+RECEIVEDTIMESTAMP+"], value ["+receivedTimestamp+"]");
|
||||
|
||||
String partnerTraceId = inboundHeaderMap.getOrDefault(PARTNER_TRACE_ID, "");
|
||||
filterHeaders.put(PARTNER_TRACE_ID, partnerTraceId);
|
||||
logger.debug("SimpleFrameworkFilter] Processing Key ["+PARTNER_TRACE_ID+"], value ["+partnerTraceId+"]");
|
||||
|
||||
String x_loan_token = inboundHeaderMap.getOrDefault(TransactionContextKeys.X_LOAN_TOKEN, "");
|
||||
filterHeaders.put(TransactionContextKeys.X_LOAN_TOKEN, x_loan_token);
|
||||
logger.debug("SimpleFrameworkFilter] Processing Key ["+TransactionContextKeys.X_LOAN_TOKEN+"], value ["+x_loan_token+"]");
|
||||
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public Map<String, String> getInboundHeaderProp(Properties prop) {
|
||||
Properties header = new Properties();
|
||||
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
try {
|
||||
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
if (headerObj instanceof Properties) { //tomcat
|
||||
header = (Properties) headerObj;
|
||||
for (String name : header.stringPropertyNames()) {
|
||||
inboundHeaderMap.put(name, header.getProperty(name));
|
||||
}
|
||||
} else if (headerObj instanceof String) { //weblogic
|
||||
String str = (String) headerObj;
|
||||
str = str.substring(1, str.length() - 1);
|
||||
// key=value 쌍 분리
|
||||
String[] entries = str.split(",");
|
||||
for (String entry : entries) {
|
||||
String[] kv = entry.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return inboundHeaderMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
|
||||
public class SimpleframeworkBodyFilter extends SimpleFrameworkFilter {
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
Object returnMessage = super.doPreFilter(adptGrpName, adptName, prop, message, tempProp);
|
||||
|
||||
JSONObject bizJsonStr = null;
|
||||
|
||||
if ( returnMessage instanceof String ) {
|
||||
bizJsonStr = (JSONObject) JSONValue.parse( (String)returnMessage);
|
||||
if( bizJsonStr != null ) {
|
||||
bizJsonStr.put("GUID", tempProp.getOrDefault(TransactionContextKeys.GUID, "") );
|
||||
}
|
||||
}
|
||||
|
||||
return bizJsonStr != null ? bizJsonStr.toJSONString() : returnMessage;
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.property.PropGroupVO;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
|
||||
public class TOSSBankFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String KJB_HEADER = "KJB_Header";
|
||||
public static final String PROP_PREFIX = "tossbank";
|
||||
public static final String X_TOSSBANK_LOAN_TRACE_ID = "X-TOSSBANK-LOAN-TRACE-ID";
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
logger.debug("TOSSBankFilter] PreFilter Processing Start!!");
|
||||
String traceId = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID, "");
|
||||
|
||||
PropGroupVO propGroupVo = PropManager.getInstance().getPropGroupVO(KJB_HEADER);
|
||||
if (propGroupVo != null) {
|
||||
}
|
||||
|
||||
Properties filterHeaders = (Properties) tempProp.get("FILTERHEADER");
|
||||
if (filterHeaders == null) {
|
||||
filterHeaders = new Properties();
|
||||
tempProp.put("FILTERHEADER", filterHeaders);
|
||||
}
|
||||
try {
|
||||
Map<String, String> inboundHeaderMap = getInboundHeaderProp(tempProp);
|
||||
|
||||
if (!inboundHeaderMap.isEmpty()) {
|
||||
if (inboundHeaderMap.containsKey(X_TOSSBANK_LOAN_TRACE_ID)) {
|
||||
filterHeaders.put(X_TOSSBANK_LOAN_TRACE_ID, inboundHeaderMap.get(X_TOSSBANK_LOAN_TRACE_ID));
|
||||
logger.debug("TOSSBankFilter] Processing Key ["+X_TOSSBANK_LOAN_TRACE_ID+"], value ["+inboundHeaderMap.get(X_TOSSBANK_LOAN_TRACE_ID)+"]");
|
||||
} else {
|
||||
filterHeaders.put(X_TOSSBANK_LOAN_TRACE_ID, traceId);
|
||||
logger.debug("TOSSBankFilter] Processing Key ["+X_TOSSBANK_LOAN_TRACE_ID+"], value ["+traceId+"]");
|
||||
}
|
||||
} else {
|
||||
/* APIM 거래추적자 */
|
||||
filterHeaders.setProperty(X_TOSSBANK_LOAN_TRACE_ID, traceId);
|
||||
logger.debug("TOSSBankFilter] Processing Key ["+X_TOSSBANK_LOAN_TRACE_ID+"], value ["+traceId+"]");
|
||||
}
|
||||
logger.debug("TOSSBankFilter] Processing Key ["+X_TOSSBANK_LOAN_TRACE_ID+"], value ["+filterHeaders.getProperty(X_TOSSBANK_LOAN_TRACE_ID)+"]");
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public Map<String, String> getInboundHeaderProp(Properties prop) {
|
||||
Properties header = new Properties();
|
||||
Map<String, String> inboundHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
try {
|
||||
Object headerObj = prop.get(HttpAdapterServiceKey.INBOUND_HEADER);
|
||||
if (headerObj instanceof Properties) { //tomcat
|
||||
header = (Properties) headerObj;
|
||||
for (String name : header.stringPropertyNames()) {
|
||||
inboundHeaderMap.put(name, header.getProperty(name));
|
||||
}
|
||||
} else if (headerObj instanceof String) { //weblogic
|
||||
String str = (String) headerObj;
|
||||
str = str.substring(1, str.length() - 1);
|
||||
// key=value 쌍 분리
|
||||
String[] entries = str.split(",");
|
||||
for (String entry : entries) {
|
||||
String[] kv = entry.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
inboundHeaderMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return inboundHeaderMap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.filter;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.nimbusds.jose.shaded.gson.JsonObject;
|
||||
|
||||
public class TraceBodyFilter implements HttpClientAdapterFilter, HttpAdapterServiceKey {
|
||||
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String BIZ_HEADER = "header";
|
||||
public static final String GUID = "guid";
|
||||
public static final String TRACEID = "traceId";
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Properties prop, Object message, Properties tempProp)
|
||||
throws Exception {
|
||||
|
||||
JSONObject bizJsonStr = null;
|
||||
|
||||
if (message instanceof String) {
|
||||
|
||||
bizJsonStr = (JSONObject) JSONValue.parse((String) message);
|
||||
|
||||
if ( bizJsonStr == null ) return message; //json string이 아님.
|
||||
|
||||
if (bizJsonStr.containsKey(BIZ_HEADER)) {
|
||||
JSONObject bizHeader = (JSONObject) bizJsonStr.get(BIZ_HEADER);
|
||||
|
||||
putBizHeaderData(bizHeader, tempProp);
|
||||
|
||||
}else {
|
||||
|
||||
JSONObject bizHeader = new JSONObject();
|
||||
bizJsonStr.put(BIZ_HEADER, bizHeader);
|
||||
putBizHeaderData(bizHeader, tempProp);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return bizJsonStr != null ? bizJsonStr.toJSONString() : message;
|
||||
}
|
||||
|
||||
static private void putBizHeaderData(JSONObject bizHeader, Properties tempProp) {
|
||||
|
||||
String traceId = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID, "");
|
||||
|
||||
String guid = tempProp.getProperty(TransactionContextKeys.GUID, "");
|
||||
|
||||
bizHeader.put(GUID, guid);
|
||||
logger.debug("Processing Key [" + GUID + "], value [" + guid + "]");
|
||||
|
||||
bizHeader.put(TRACEID, traceId);
|
||||
logger.debug("Processing Key [" + TRACEID + "], value [" + traceId + "]");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Properties prop, Object message,
|
||||
Properties tempProp) throws Exception {
|
||||
// TODO Auto-generated method stub
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
-897
@@ -1,897 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.kjb;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.ConnectException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.time.StopWatch;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.apache.hc.client5.http.classic.HttpClient;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpDelete;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpGet;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpPost;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpPut;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
|
||||
import org.apache.hc.core5.http.*;
|
||||
import org.apache.hc.core5.http.io.entity.EntityUtils;
|
||||
import org.apache.hc.core5.http.io.entity.StringEntity;
|
||||
import org.apache.hc.core5.http.message.BasicNameValuePair;
|
||||
import org.apache.hc.core5.http.protocol.BasicHttpContext;
|
||||
import org.apache.hc.core5.http.protocol.HttpContext;
|
||||
import org.apache.hc.core5.net.URIBuilder;
|
||||
import org.dom4j.Document;
|
||||
import org.dom4j.Element;
|
||||
import org.dom4j.io.SAXReader;
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
import org.json.simple.parser.JSONParser;
|
||||
import org.json.simple.parser.ParseException;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import com.eactive.eai.adapter.http.HttpMemoryLogger;
|
||||
import com.eactive.eai.adapter.http.HttpMethodType;
|
||||
import com.eactive.eai.adapter.http.client.HttpClient5AdapterServiceSupport;
|
||||
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey;
|
||||
import com.eactive.eai.adapter.http.client.HttpClientAdapterVO;
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.authoutbound.AccessTokenManagerByDB;
|
||||
import com.eactive.eai.common.exception.ExceptionUtil;
|
||||
import com.eactive.eai.common.message.MessageType;
|
||||
import com.eactive.eai.common.util.CommonLib;
|
||||
import com.jayway.jsonpath.DocumentContext;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.openbanking.eai.common.token.AccessTokenManager;
|
||||
import com.openbanking.eai.common.token.AccessTokenVO;
|
||||
import com.openbanking.eai.common.token.OAuth2AccessTokenVO;
|
||||
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
|
||||
public class HttpClient5AdapterServiceBase64 extends HttpClient5AdapterServiceSupport
|
||||
implements HttpClientAdapterServiceKey {
|
||||
|
||||
public static final String TYPE_VARIABLE_URL_REQUEST = "variableUrlRequest";
|
||||
public static final String TYPE_SIMPLE_REQUEST = "simpleRequest";
|
||||
public static final String REST_OPTION = "REST_OPTION";
|
||||
//public static final String HEADER_CONTENT_TYPE = "Content-Type";
|
||||
public static final String HEADER_GROUP = "HEADER_GROUP";
|
||||
public static final String HTTP_STATUS = "HTTP_STATUS";
|
||||
public static final String HEADER_KEYS = "HEADER_KEYS";
|
||||
public static final String AUTH_TOKEN = "AUTH_TOKEN";
|
||||
|
||||
/**
|
||||
* 1. 기능 : REST API 통신에 사용 <br>
|
||||
* 2. 처리 개요 : - 속성 정보를 설정 하고 수동 시스템 서비스를 호출 한다. <br>
|
||||
* 3. 주의사항 <br>
|
||||
*
|
||||
* @param prop Http Adapter 속성 정보
|
||||
* @return 반환 된 Object
|
||||
* @exception Exception 수동 시스템 간 통신 중 발생
|
||||
**/
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
public Object execute(Properties prop, Object data, Properties tempProp) throws Exception {
|
||||
HttpClientAdapterVO vo = super.setting(prop, tempProp);
|
||||
|
||||
// 2025.04.09 Authorization 헤더값을 프로퍼티(AUTH_TOKEN)에서 지정하여 설정할 수 있도록 수정
|
||||
String bizCode = "";
|
||||
String authToken = "";
|
||||
try {
|
||||
Properties authProp = PropManager.getInstance().getProperties(AUTH_TOKEN); //Authorization
|
||||
bizCode = vo.getAdapterGroupName().substring(1, 4);
|
||||
authToken = authProp.getProperty(bizCode, "");
|
||||
} catch (Exception e) {
|
||||
logger.debug("Property Group AUTH_TOKEN is not Exist !!");
|
||||
}
|
||||
|
||||
logger.debug("authToken :::::::::::::::::::::::::::::::::::" + authToken);
|
||||
|
||||
String headerGroupName = prop.getProperty(HEADER_GROUP);
|
||||
String relayResponseHeaderKeys = prop.getProperty(HEADER_KEYS, "");
|
||||
boolean useAdapterToken = StringUtils.equalsIgnoreCase(prop.getProperty("ADAPTER_TOKEN_USE_YN", "N"), "Y");
|
||||
|
||||
boolean useForwardProxy = StringUtils.equalsIgnoreCase(prop.getProperty("FORWARD_PROXY_USE_YN", "N"), "Y");
|
||||
String forwardProxyUrl = prop.getProperty("FORWARD_PROXY_URL");
|
||||
|
||||
// ex) $.dataHeader.GW_RSLT_CD
|
||||
String tokenErrorCodeKey = prop.getProperty("ADAPTER_TOKEN_ERROR_CODE_KEY");
|
||||
String tokenErrorCodeValues = prop.getProperty("ADAPTER_TOKEN_ERROR_CODE_VALUES");
|
||||
String tokenErrorHttpStatusCode = prop.getProperty("ADAPTER_TOKEN_ERROR_HTTP_STATUS_CODE"); // 200 or 400번대
|
||||
|
||||
// 레이아웃 메시지 타입 REST URL 추출 시 활용. Default JSON
|
||||
String messageType = prop.getProperty("MESSAGE_TYPE", MessageType.JSON);
|
||||
String inboundMethod = tempProp.getProperty(HttpAdapterServiceKey.INBOUND_METHOD, "POST");
|
||||
|
||||
/**
|
||||
* @formatter:off
|
||||
* TSEAIHE02.RESTOPTION 정보 => JSON 형태로 구성
|
||||
* - type : simpleRequest, variableUrlRequest
|
||||
* - extraPath : 어댑터 프로퍼티 URL에 추가될 HTTP REST URL
|
||||
* - method : get, delete, post, put
|
||||
* - contentType: application/json(default), application/x-www-form-urlencoded
|
||||
* - adapterTokenUseYn: Y, N(default)
|
||||
* ex)
|
||||
* {"type":"simpleRequest","extraPath":"v2.0/accout/balance","method":"post"}
|
||||
* @formatter:on
|
||||
*/
|
||||
|
||||
String restOptionData = tempProp.getProperty(REST_OPTION);
|
||||
|
||||
JSONObject restOptionObject = parseJson(restOptionData);
|
||||
if (restOptionObject == null) {
|
||||
throw new Exception("OptionData parsing result is NULL");
|
||||
}
|
||||
|
||||
String rmethod = (String) restOptionObject.getOrDefault("method", inboundMethod);
|
||||
String contentType = (String) restOptionObject.getOrDefault("contentType", "application/json");
|
||||
String adapterTokenUseYn = (String) restOptionObject.get("adapterTokenUseYn");
|
||||
if (StringUtils.isNotBlank(adapterTokenUseYn)) { // interface 에 설정된게 우선한다.
|
||||
useAdapterToken = StringUtils.equalsIgnoreCase(adapterTokenUseYn, "Y");
|
||||
}
|
||||
|
||||
HttpClient mclient = this.client;
|
||||
String sendData = "";
|
||||
if (data instanceof String) {
|
||||
sendData = (String) data;
|
||||
} else if (data instanceof byte[]) {
|
||||
sendData = new String((byte[]) data, vo.getEncode());
|
||||
}
|
||||
|
||||
////mclient.getParams().setContentCharset(vo.getEncode());
|
||||
|
||||
JSONObject dataObject = null;
|
||||
|
||||
Object httpHeader = null;
|
||||
Object dataContent = null;
|
||||
|
||||
if (MessageType.JSON.equals(messageType)) {
|
||||
|
||||
Object parsed = parseJsonGeneric(sendData);
|
||||
|
||||
if(parsed instanceof JSONObject) {
|
||||
dataObject = parseJson(sendData);
|
||||
if (dataObject != null && StringUtils.isNotBlank(headerGroupName)) {
|
||||
httpHeader = dataObject.get(headerGroupName);
|
||||
dataObject.remove(headerGroupName);
|
||||
}
|
||||
|
||||
dataContent = dataObject;
|
||||
if (dataObject.containsKey("innerList")) {
|
||||
JSONArray innerList = (JSONArray) dataObject.get("innerList");
|
||||
dataContent = innerList;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("HttpClientAdapterServiceRest] dataObject1 = [" + dataObject + "]");
|
||||
logger.debug("HttpClientAdapterServiceRest] dataContent = [" + dataContent + "]");
|
||||
|
||||
// interface 에 설정된게 우선한다.
|
||||
String uri = null;
|
||||
if (dataObject == null) {
|
||||
uri = changeUrl(messageType, vo.getUrl(), restOptionData, sendData);
|
||||
} else {
|
||||
uri = changeUrl(messageType, vo.getUrl(), restOptionData, dataObject);
|
||||
}
|
||||
|
||||
// ////if (vo.getConnectionTimeout() != 0) {
|
||||
// mclient.getHttpConnectionManager().getParams().setConnectionTimeout(vo.getConnectionTimeout());
|
||||
// }
|
||||
|
||||
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") URL=[" + vo.getUrl() + "] ");
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") PARAMETER_NAME=[" + vo.getParameterName() + "] ");
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") ENCODE=[" + vo.getEncode() + "] ");
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") RESPONSE_TYPE=[" + vo.getResponseType() + "] ");
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") URL_ENCODE_YN=[" + vo.getUrlEncodeYn() + "] ");
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") CONNECTION_TIMEOUT=[" + vo.getConnectionTimeout() + "] ");
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") REST_OPTION=[" + restOptionData + "] ");
|
||||
}
|
||||
|
||||
HttpUriRequestBase method = null;
|
||||
|
||||
// 메소드 타입에 따라 HttpRequestBase 인스턴스 생성
|
||||
switch (HttpMethodType.getValue(rmethod)) {
|
||||
case GET:
|
||||
method = new HttpGet(uri);
|
||||
break;
|
||||
case DELETE:
|
||||
method = new HttpDelete(uri);
|
||||
break;
|
||||
case POST:
|
||||
method = new HttpPost(uri);
|
||||
break;
|
||||
case PUT:
|
||||
method = new HttpPut(uri);
|
||||
break;
|
||||
default:
|
||||
method = new HttpGet(uri);
|
||||
break;
|
||||
}
|
||||
|
||||
RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
|
||||
|
||||
if(useForwardProxy) {
|
||||
java.net.URL url = new java.net.URL(forwardProxyUrl);
|
||||
|
||||
// 프로토콜, 호스트, 포트 추출
|
||||
String protocol = url.getProtocol();
|
||||
String host = url.getHost();
|
||||
int port = url.getPort();
|
||||
HttpHost proxy = new HttpHost(protocol,host, port);
|
||||
requestConfigBuilder.setProxy(proxy);
|
||||
}
|
||||
|
||||
configureHttpClient(requestConfigBuilder, vo, method);
|
||||
|
||||
switch (HttpMethodType.getValue(rmethod)) {
|
||||
case GET:
|
||||
case DELETE:
|
||||
HashMap<String, String> h = getParameters(dataObject);
|
||||
URIBuilder uriBuilder = new URIBuilder(uri);
|
||||
for (Map.Entry<String, String> entry : h.entrySet()) {
|
||||
uriBuilder.addParameter(entry.getKey(), entry.getValue());
|
||||
}
|
||||
URI fullUri = uriBuilder.build();
|
||||
if (method instanceof HttpGet) {
|
||||
((HttpGet) method).setUri(fullUri);
|
||||
} else if (method instanceof HttpDelete) {
|
||||
((HttpDelete) method).setUri(fullUri);
|
||||
}
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("HttpClientAdapterServiceRest] (get Method) QueryString = [" + fullUri.getQuery() + "]");
|
||||
}
|
||||
break;
|
||||
case PUT:
|
||||
case POST:
|
||||
//assignPostBody(dataObject, contentType, vo.getEncode(), method);
|
||||
assignPostBody(dataContent, contentType, vo.getEncode(), method);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") method.getParams()=["
|
||||
+ method.getEntity() + "] ");
|
||||
}
|
||||
|
||||
OAuth2AccessTokenVO accessToken = null;
|
||||
if (useAdapterToken) {
|
||||
AccessTokenManagerByDB tokenManager = AccessTokenManagerByDB.getInstance();
|
||||
accessToken = (OAuth2AccessTokenVO) tokenManager.getAccessTokenVO(vo.getAdapterGroupName());
|
||||
|
||||
// 토큰이 없거나 만료 됐으면 재발급
|
||||
if (accessToken == null || accessToken.isExpired()) {
|
||||
String oldToken = accessToken == null ? null : accessToken.getAccessToken();
|
||||
accessToken = (OAuth2AccessTokenVO) tokenManager.retryAccessTokenVO(vo.getAdapterGroupName(), prop,
|
||||
oldToken);
|
||||
}
|
||||
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") TOKEN = ["
|
||||
+ accessToken + "]");
|
||||
}
|
||||
|
||||
setAuthHeaders(method, accessToken);
|
||||
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName()
|
||||
+ ") RequestHeader [Authorization=" + method.getHeader("Authorization") + "]");
|
||||
}
|
||||
}
|
||||
|
||||
// 2025.04.09 Authorization 헤더값을 프로퍼티(AUTH_TOKEN)에서 지정하여 설정할 수 있도록 수정
|
||||
if(!StringUtils.isBlank(authToken)) {
|
||||
setAuthHeaders(method, authToken);
|
||||
}
|
||||
|
||||
// 전달 header 셋팅
|
||||
// Adapter 레벨 header 보다 data로 넘어온 httpHeader를 우선 적용(header명이 같다면 덮어씀)
|
||||
assignRequestHeaders(method, httpHeader);
|
||||
|
||||
int status = -1;
|
||||
|
||||
try {
|
||||
byte[] responseMessage = null;
|
||||
Header[] responseHeaders;
|
||||
|
||||
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
|
||||
if (dataContent != null) { //dataObject
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") = ["
|
||||
+ dataContent.toString() + "]"); //dataObject.toJSONString()
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") = "
|
||||
+ CommonLib.getDumpMessage(dataContent.toString())); //dataObject.toJSONString()
|
||||
} else {
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") = [" + sendData
|
||||
+ "]");
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") = "
|
||||
+ CommonLib.getDumpMessage(sendData));
|
||||
}
|
||||
}
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
stopWatch.start();
|
||||
|
||||
if (vo.getTraceLevel() >= 3) {
|
||||
HttpMemoryLogger.txlog(vo.getAdapterGroupName() + vo.getAdapterName(),
|
||||
"SEND [" + sendData + "]" + CommonLib.getDumpMessage(sendData));
|
||||
}
|
||||
|
||||
String uuid = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID);
|
||||
HttpContext context = new BasicHttpContext();
|
||||
context.setAttribute(TransactionContextKeys.TRANSACTION_UUID, uuid);
|
||||
context.setAttribute(HttpClientAdapterServiceKey.ADAPTER_GROUP_NAME, vo.getAdapterGroupName());
|
||||
context.setAttribute(HttpClientAdapterServiceKey.ADAPTER_NAME, vo.getAdapterName());
|
||||
Integer logProcessNo = (Integer) tempProp.get(HttpClientAdapterServiceKey.LOG_PROCESS_NO);
|
||||
if (logProcessNo != null && logProcessNo > 0) {
|
||||
context.setAttribute(HttpClientAdapterServiceKey.LOG_PROCESS_NO, logProcessNo);
|
||||
}
|
||||
|
||||
try {
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("[method getName]" + method.getMethod());
|
||||
logger.debug("[method getEntity]" + method.getEntity());
|
||||
logger.debug("[method getRequestHeaders]" + method.getHeaders().toString());
|
||||
logger.debug("[method getURI]" + method.getPath());
|
||||
}
|
||||
try (CloseableHttpResponse response = (CloseableHttpResponse) mclient.execute(method, context)) {
|
||||
status = response.getCode();
|
||||
|
||||
if (status >= 200 && status <= 207) {
|
||||
status = 200;
|
||||
}
|
||||
|
||||
// if (status == 404) {
|
||||
// status = 200;
|
||||
// }
|
||||
|
||||
// responseMessage = EntityUtils.toByteArray(response.getEntity());
|
||||
responseHeaders = response.getHeaders();
|
||||
// 2025/08/06 Response body를 Base64로 Encode 해서 Json으로 만들어서 전달하도록 수정
|
||||
HttpEntity entity = response.getEntity();
|
||||
if (entity != null) {
|
||||
byte[] entityBytes = EntityUtils.toByteArray(response.getEntity());
|
||||
String b64 = Base64.encodeBase64String(entityBytes);
|
||||
JSONObject resultJson = new JSONObject();
|
||||
resultJson.put("base64_file_data", b64);
|
||||
responseMessage = resultJson.toString().getBytes();
|
||||
} else {
|
||||
responseMessage = EntityUtils.toByteArray(response.getEntity());
|
||||
}
|
||||
|
||||
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("[received status]" + status);
|
||||
logger.debug("HttpClientAdapterServiceRest] RECV (" + vo.getAdapterGroupName() + ") = [" + responseMessage + "]");
|
||||
}
|
||||
}
|
||||
} catch (ConnectException e) {
|
||||
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
|
||||
logger.debug("HttpClientAdapterServiceRest] responseType =" + vo.getResponseType());
|
||||
}
|
||||
throw new Exception("excuteMethod java.net.ConnectException = " + e.getMessage());
|
||||
}
|
||||
|
||||
stopWatch.stop();
|
||||
|
||||
/**
|
||||
* @formatter:off
|
||||
* OAuth 토큰 응답 체크(Adapter properties로 설정)
|
||||
* 응답코드에 따라 유효한 토큰 확인(토큰 재발급 여부 확인)
|
||||
* API 서비스 마다 정책이 다름(보통 400대에서 체크하나 200에서도 체크할 수 있음)
|
||||
*
|
||||
* TOKEN_ERROR_CODE_KEY: 응답 json error code key
|
||||
* __ex) $.dataHeader.resultCode
|
||||
* TOKEN_ERROR_CODE_VALUES: 토큰 재발급이 필요한 응답 error codes(ex: O0001,O0002,O0003)
|
||||
* TOKEN_ERROR_HTTP_STATUS_CODE: 보통 400대에서 체크하나 API 제공자에 따라 200에서도 체클할수 있음
|
||||
* ex) TOKEN_ERROR_HTTP_STATUS_CODE = 200
|
||||
* @formatter:on
|
||||
*/
|
||||
boolean needReissue = false;
|
||||
|
||||
Properties responseHeaderProp = assignRelayDataToInbound(tempProp, responseHeaders);
|
||||
String responseString = new String(responseMessage, vo.getEncode());
|
||||
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
tempProp.put(HttpAdapterServiceKey.OUTBOUND_PROPERTY_MAP, map);
|
||||
map.put(HttpAdapterServiceKey.OUTBOUND_RESPONSE_HEADERS, responseHeaderProp);
|
||||
map.put(HttpAdapterServiceKey.OUTBOUND_RESPONSE_MESSAGE, responseString);
|
||||
map.put(HttpAdapterServiceKey.OUTBOUND_RESPONSE_STATUS, status);
|
||||
|
||||
if (status >= 200 && status <= 207) {
|
||||
if (useAdapterToken && StringUtils.equals(tokenErrorHttpStatusCode, "200")) {
|
||||
needReissue = checkTokenRetry(responseMessage, tokenErrorCodeKey, tokenErrorCodeValues,
|
||||
vo.getEncode());
|
||||
}
|
||||
} else if (status == 302) {
|
||||
if (!StringUtils.contains(relayResponseHeaderKeys, "Location")) {
|
||||
relayResponseHeaderKeys += ",Location";
|
||||
}
|
||||
} else {
|
||||
String errMsg = String.format(
|
||||
"http receive status fail value=%d adapterName=%s.%s uri=%s method=%s response Data=%s",
|
||||
status, vo.getAdapterGroupName(), vo.getAdapterName(), uri, rmethod, responseString);
|
||||
// logger.error(errMsg);
|
||||
|
||||
if (status >= 400 && status < 500) {
|
||||
if (useAdapterToken) {
|
||||
needReissue = checkTokenRetry(responseMessage, tokenErrorCodeKey, tokenErrorCodeValues,
|
||||
vo.getEncode());
|
||||
}
|
||||
|
||||
// if (!needReissue) {
|
||||
// throw new Exception(errMsg);
|
||||
// }
|
||||
} else {
|
||||
|
||||
throw new Exception(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
|
||||
logger.debug("HttpClientAdapterServiceRest] responseType =" + vo.getResponseType());
|
||||
logger.debug("HttpClientAdapterServiceRest] RECV " + vo.getEncode() + "(" + vo.getAdapterGroupName() + ") = [" + new String(responseMessage, vo.getEncode()) + "]");
|
||||
// logger.debug("HttpClientAdapterServiceRest] RECV MS949 (" + vo.getAdapterGroupName() + ") = [" + new String(responseMessage, "MS949") + "]");
|
||||
// logger.debug("HttpClientAdapterServiceRest] RECV euc-kr (" + vo.getAdapterGroupName() + ") = [" + new String(responseMessage, "euc-kr") + "]");
|
||||
logger.debug("HttpClientAdapterServiceRest] RECV (" + vo.getAdapterGroupName() + ") = " + CommonLib.getDumpMessage(responseMessage));
|
||||
}
|
||||
|
||||
if (useAdapterToken) {
|
||||
if (responseMessage == null) {
|
||||
throw new Exception("responseMessage is NULL, HttpStatus=" + status);
|
||||
}
|
||||
|
||||
// OAuth 토큰 재요청 코드 확인
|
||||
if (needReissue) {
|
||||
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
|
||||
logger.debug("HttpClientAdapterServiceRest] retry access token response code = ["
|
||||
+ vo.getResponseType() + "] message = [" + new String(responseMessage, vo.getEncode())
|
||||
+ "]");
|
||||
}
|
||||
|
||||
// 토큰 재발급
|
||||
AccessTokenManager tokenManager = AccessTokenManager.getInstance();
|
||||
String oldToken = accessToken == null ? null : accessToken.getAccessToken();
|
||||
OAuth2AccessTokenVO newaccessToken = (OAuth2AccessTokenVO) tokenManager
|
||||
.retryAccessTokenVO(vo.getAdapterGroupName(), prop, oldToken);
|
||||
method.setHeader("Authorization", newaccessToken.getAuthorization());
|
||||
|
||||
setAuthHeaders(method, newaccessToken);
|
||||
|
||||
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName()
|
||||
+ ") RETRY TOKEN = [" + newaccessToken + "]");
|
||||
}
|
||||
|
||||
try (CloseableHttpResponse response = (CloseableHttpResponse) mclient.execute(method)) {
|
||||
status = response.getCode();
|
||||
responseMessage = EntityUtils.toByteArray(response.getEntity());
|
||||
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("[received status]" + status);
|
||||
logger.debug("HttpClientAdapterServiceRest] RECV (" + vo.getAdapterGroupName() + ") = [" + new String(responseMessage, vo.getEncode()) + "]");
|
||||
}
|
||||
}catch (IOException e){
|
||||
throw new Exception("retry excuteMethod Exceptioin = " + e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
if (status == 302) {
|
||||
if (!StringUtils.contains(relayResponseHeaderKeys, "Location")) {
|
||||
relayResponseHeaderKeys += ",Location";
|
||||
}
|
||||
} else {
|
||||
String errMsg = String.format(
|
||||
"http receive status fail value=%d adapterName=%s.%s uri=%s method=%s response Data=%s",
|
||||
status, vo.getAdapterGroupName(), vo.getAdapterName(), uri, rmethod, responseMessage);
|
||||
throw new Exception(errMsg);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
|
||||
logger.debug("HttpClientAdapterServiceRest] RETRY responseType =" + vo.getResponseType());
|
||||
logger.debug("HttpClientAdapterServiceRest] RETRY RECV " + vo.getEncode() + "("
|
||||
+ vo.getAdapterGroupName() + ") = [" + new String(responseMessage, vo.getEncode())
|
||||
+ "]");
|
||||
logger.debug("HttpClientAdapterServiceRest] RETRY RECV (" + vo.getAdapterGroupName() + ") = "
|
||||
+ CommonLib.getDumpMessage(responseMessage));
|
||||
}
|
||||
// ----------------------------------------------------------
|
||||
}
|
||||
}
|
||||
|
||||
if (vo.getTraceLevel() >= 3) {
|
||||
HttpMemoryLogger.txlog(vo.getAdapterGroupName() + vo.getAdapterName(),
|
||||
"RECV [" + new String(responseMessage, vo.getEncode()) + "]"
|
||||
+ CommonLib.getDumpMessage(responseMessage));
|
||||
}
|
||||
|
||||
|
||||
return assignResponseHeaders(method, responseMessage, headerGroupName, vo.getEncode(), messageType,
|
||||
relayResponseHeaderKeys, status, responseHeaderProp);
|
||||
} catch (SocketTimeoutException ste) { // Read Timeout :: HttpClient 3.1일 경우
|
||||
if (vo.getTraceLevel() >= 3) {
|
||||
HttpMemoryLogger.error(vo.getAdapterGroupName() + vo.getAdapterName(),
|
||||
"HttpClientAdapterServiceRest] SocketTimeoutException (" + vo.getAdapterGroupName() + ") : "
|
||||
+ ste.toString(),
|
||||
ste);
|
||||
}
|
||||
logger.error("HttpClientAdapterServiceRest] SocketTimeoutException (" + vo.getAdapterGroupName() + ") : "
|
||||
+ ste.toString(), ste);
|
||||
throw ste;
|
||||
} catch (ConnectException ce) {
|
||||
if (vo.getTraceLevel() >= 3) {
|
||||
HttpMemoryLogger.error(vo.getAdapterGroupName() + vo.getAdapterName(),
|
||||
"HttpClientAdapterServiceRest] Connection Exception (" + vo.getAdapterGroupName() + ") : "
|
||||
+ ce.toString(),
|
||||
ce);
|
||||
}
|
||||
logger.error("HttpClientAdapterServiceRest] Connection Exception (" + vo.getAdapterGroupName() + ") : "
|
||||
+ ce.toString(), ce);
|
||||
throw ce;
|
||||
} catch (Exception e) {
|
||||
if (vo.getTraceLevel() >= 3) {
|
||||
HttpMemoryLogger.error(vo.getAdapterGroupName() + vo.getAdapterName(), e.toString(), e);
|
||||
}
|
||||
logger.error("HttpClientAdapterServiceRest] Exception (" + vo.getAdapterGroupName() + ") : " + e.toString(),
|
||||
e);
|
||||
throw e;
|
||||
} finally {
|
||||
if (status != HttpStatus.SC_OK) {
|
||||
String[] msgArgs = new String[1];
|
||||
msgArgs[0] = String.valueOf(status);
|
||||
String resMsg = ExceptionUtil.make("RDCEAIAHA013", msgArgs);
|
||||
if (logger.isDebug()) {
|
||||
logger.debug(resMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Properties assignRelayDataToInbound(Properties prop, Header[] responseHeaders) {
|
||||
Properties headerProp = new Properties();
|
||||
if (responseHeaders == null || responseHeaders.length == 0) {
|
||||
return headerProp;
|
||||
}
|
||||
|
||||
for (Header header : responseHeaders) {
|
||||
headerProp.put(header.getName(), header.getValue());
|
||||
}
|
||||
|
||||
return headerProp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 1단계 레이아웃에만 적용되도록 구현됨.
|
||||
*
|
||||
* @param method
|
||||
* @param responseMessage
|
||||
* @param headerGroupName
|
||||
* @param encode
|
||||
* @param messageType
|
||||
* @param relayHeaderKeys
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private String assignResponseHeaders(HttpUriRequestBase method, byte[] responseMessage, String headerGroupName,
|
||||
String encode, String messageType, String relayHeaderKeys, int status, Properties responseHeaderProp) throws Exception {
|
||||
if (!MessageType.JSON.equals(messageType) || StringUtils.isBlank(headerGroupName)
|
||||
|| StringUtils.isBlank(relayHeaderKeys)) {
|
||||
return new String(responseMessage, encode);
|
||||
}
|
||||
|
||||
String[] relayKeyArr = org.springframework.util.StringUtils.tokenizeToStringArray(relayHeaderKeys, ",");
|
||||
JSONObject headerJson = new JSONObject();
|
||||
for (String key : relayKeyArr) {
|
||||
String value = responseHeaderProp.getProperty(key);
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
headerJson.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
headerJson.put(HTTP_STATUS, String.valueOf(status));
|
||||
|
||||
if (headerJson.size() <= 0) {
|
||||
return new String(responseMessage, encode);
|
||||
}
|
||||
|
||||
JSONObject message = null;
|
||||
if (responseMessage == null || responseMessage.length == 0) {
|
||||
message = new JSONObject();
|
||||
} else {
|
||||
message = parseJson(new String(responseMessage, encode));
|
||||
if (message == null) {
|
||||
message = new JSONObject();
|
||||
message.put("Malformed_Response_Message", new String(responseMessage, encode));
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(headerGroupName) && message != null)
|
||||
message.put(headerGroupName, headerJson);
|
||||
|
||||
logger.info("------------- message --------------- : {} ", message);
|
||||
|
||||
return message.toJSONString();
|
||||
}
|
||||
|
||||
// 2025.04.09 Authorization 헤더값을 프로퍼티(AUTH_TOKEN)에서 지정하여 설정할 수 있도록 수정
|
||||
private void setAuthHeaders(HttpUriRequestBase method, String authorization) throws Exception {
|
||||
method.setHeader("Authorization",
|
||||
String.format("%s", authorization));
|
||||
}
|
||||
|
||||
private void setAuthHeaders(HttpUriRequestBase method, OAuth2AccessTokenVO accessToken) throws Exception {
|
||||
method.setHeader("Authorization",
|
||||
String.format("%s %s", AccessTokenVO.BEARER_TYPE, accessToken.getAccessToken()));
|
||||
}
|
||||
|
||||
private boolean checkTokenRetry(byte[] responseMessage, String tokenErrorCodeKey, String tokenErrorCodeValues,
|
||||
String encode) {
|
||||
if (responseMessage == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(tokenErrorCodeKey)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(tokenErrorCodeValues)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
DocumentContext jsonContext = JsonPath.parse(new String(responseMessage, encode));
|
||||
String responseCode = jsonContext.read(tokenErrorCodeKey);
|
||||
if (StringUtils.isBlank(responseCode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String[] arr = org.springframework.util.StringUtils.tokenizeToStringArray(tokenErrorCodeValues, ",");
|
||||
return ArrayUtils.contains(arr, responseCode);
|
||||
} catch (Exception e) {
|
||||
logger.error("HttpClientAdapterServiceRest] checkTokenRetry error=" + e.getMessage());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void assignRequestHeaders(HttpUriRequestBase method, Object httpHeader) {
|
||||
if (httpHeader == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (httpHeader instanceof JSONObject) {
|
||||
JSONObject jsonObject = (JSONObject) httpHeader;
|
||||
for (Object key : jsonObject.keySet()) {
|
||||
Object obj = jsonObject.get(key);
|
||||
if ((obj instanceof JSONObject) || (obj instanceof JSONArray)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
method.setHeader((String) key, (String) obj);
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Request Header :" + (String) key + "=[" + (String) obj + "]");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void assignPostBody(Object eaiBody, String contentType, String charset, HttpUriRequestBase method) {
|
||||
if (eaiBody == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (StringUtils.contains(contentType, "application/x-www-form-urlencoded")) {
|
||||
if (eaiBody instanceof JSONObject) {
|
||||
List<NameValuePair> params = new ArrayList<>();
|
||||
JSONObject jsonObject = (JSONObject) eaiBody;
|
||||
for (Object key : jsonObject.keySet()) {
|
||||
params.add(new BasicNameValuePair((String) key, (String) jsonObject.get(key)));
|
||||
}
|
||||
|
||||
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, StandardCharsets.UTF_8);
|
||||
method.setEntity(entity);
|
||||
}
|
||||
|
||||
} else {
|
||||
ContentType contentTypeObj = ContentType.create(contentType, charset);
|
||||
|
||||
// 요청 본문을 StringEntity 객체로 생성
|
||||
StringEntity entity = new StringEntity(eaiBody.toString(), contentTypeObj);
|
||||
|
||||
// 요청에 본문 추가
|
||||
method.setEntity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
private HashMap<String, String> getParameters(Object message) throws Exception {
|
||||
|
||||
HashMap<String, String> result = new HashMap<String, String>();
|
||||
|
||||
if (message != null) {
|
||||
if (message instanceof JSONObject) {
|
||||
JSONObject jsonObject = (JSONObject) message;
|
||||
for (Object key : jsonObject.keySet()) {
|
||||
Object obj = jsonObject.get(key);
|
||||
if ((obj instanceof JSONObject) || (obj instanceof JSONArray)) {
|
||||
continue;
|
||||
}
|
||||
result.put((String) key, getStringValue(obj));
|
||||
}
|
||||
} else {
|
||||
String[] messages = ((String) message).split("&");
|
||||
String[] data = null;
|
||||
|
||||
for (int i = 0; i < messages.length; i++) {
|
||||
data = messages[i].split("=", 2);
|
||||
if (data.length == 2) {
|
||||
result.put(data[0], data[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public String getStringValue(Object obj) {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (obj instanceof String) {
|
||||
return (String) obj;
|
||||
} else if (obj instanceof Long) {
|
||||
return Long.toString((Long) obj);
|
||||
} else if (obj instanceof BigDecimal) {
|
||||
return ((BigDecimal) obj).toPlainString();
|
||||
} else {
|
||||
return (String) obj;
|
||||
}
|
||||
}
|
||||
|
||||
public static Object parseJsonGeneric(String jsonData) {
|
||||
if (StringUtils.isBlank(jsonData)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
JSONParser parser = new JSONParser();
|
||||
Object parsedObj = parser.parse(jsonData);
|
||||
return parsedObj;
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private JSONObject parseJson(String message) throws Exception {
|
||||
if (message == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (JSONObject) JSONValue.parse(message);
|
||||
}
|
||||
|
||||
private Document convertXmlDocument(String message) throws Exception {
|
||||
SAXReader builder = new SAXReader();
|
||||
Document document = builder.read(new StringReader(message));
|
||||
return document;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private String changeUrl(String messageType, String url, String restOption, Object sendData) {
|
||||
if ((MessageType.JSON.equals(messageType) || MessageType.XML.equals(messageType))) {
|
||||
try {
|
||||
if (StringUtils.isBlank(restOption)) {
|
||||
return url;
|
||||
}
|
||||
List<String> urlVariableList = new ArrayList<String>();
|
||||
JSONObject restOptionObject = parseJson(restOption);
|
||||
|
||||
if (restOptionObject == null)
|
||||
throw new Exception("restOption is NULL");
|
||||
|
||||
String type = (String) restOptionObject.get("type");
|
||||
String requestExtraPath = (String) restOptionObject.get("extraPath");
|
||||
url = getUrl(url, requestExtraPath);
|
||||
if (TYPE_VARIABLE_URL_REQUEST.equals(type)) {
|
||||
Pattern p = Pattern.compile("\\{(.*?)\\}");
|
||||
Matcher m = p.matcher(requestExtraPath);
|
||||
List<String> uriVariables = new ArrayList<>();
|
||||
while(m.find()) {
|
||||
String fieldName = m.group(1);
|
||||
uriVariables.add(fieldName);
|
||||
}
|
||||
if (MessageType.JSON.equals(messageType)) {
|
||||
JSONObject jsonObject;
|
||||
if (sendData instanceof JSONObject) {
|
||||
jsonObject = (JSONObject) sendData;
|
||||
} else {
|
||||
jsonObject = (JSONObject) JSONValue.parse((String) sendData);
|
||||
}
|
||||
for (Object tempObject : uriVariables) {
|
||||
String urlVaribleId = (String) tempObject;
|
||||
String uriVariable = (String) jsonObject.get(urlVaribleId);
|
||||
urlVariableList.add(uriVariable);
|
||||
jsonObject.remove(urlVaribleId);
|
||||
}
|
||||
} else if (MessageType.XML.equals(messageType)) {
|
||||
Document doc;
|
||||
if (sendData instanceof Document) {
|
||||
doc = (Document) sendData;
|
||||
} else {
|
||||
doc = convertXmlDocument((String) sendData);
|
||||
}
|
||||
for (Object tempObject : uriVariables) {
|
||||
String urlVaribleId = (String) tempObject;
|
||||
Element element = (Element) doc.selectSingleNode("//" + urlVaribleId);
|
||||
urlVariableList.add(element.getText());
|
||||
if (doc.getRootElement() == element) {
|
||||
doc = null;
|
||||
} else {
|
||||
element.getParent().remove(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (urlVariableList.size() > 0) {
|
||||
UriComponents uriComponents = UriComponentsBuilder.fromUriString(url).build();
|
||||
Object[] urls = urlVariableList.toArray();
|
||||
url = uriComponents.expand(urls).toUriString();
|
||||
logger.debug("HttpClientAdapterServiceRest] after extand url=[" + url + "] ");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to change url", e);
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
private String getUrl(String baseUrl, String extraPath) {
|
||||
if (StringUtils.isBlank(extraPath)) {
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
if (StringUtils.contains(extraPath, "http://") || StringUtils.contains(extraPath, "https://")) {
|
||||
return extraPath;
|
||||
}
|
||||
|
||||
String targetUrl = baseUrl;
|
||||
if (!targetUrl.endsWith("/")) {
|
||||
targetUrl += "/";
|
||||
}
|
||||
if (extraPath.startsWith("/")) {
|
||||
targetUrl += extraPath.substring(1);
|
||||
} else {
|
||||
targetUrl += extraPath;
|
||||
}
|
||||
|
||||
logger.debug("HttpClientAdapterServiceRest] after concatenate url=[" + targetUrl + "] ");
|
||||
return targetUrl;
|
||||
}
|
||||
|
||||
}
|
||||
-888
@@ -1,888 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.client.impl.kjb;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.net.ConnectException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.time.StopWatch;
|
||||
import org.apache.hc.client5.http.classic.HttpClient;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpDelete;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpGet;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpPost;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpPut;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
|
||||
import org.apache.hc.core5.http.ContentType;
|
||||
import org.apache.hc.core5.http.Header;
|
||||
import org.apache.hc.core5.http.HttpHost;
|
||||
import org.apache.hc.core5.http.HttpStatus;
|
||||
import org.apache.hc.core5.http.NameValuePair;
|
||||
import org.apache.hc.core5.http.io.entity.EntityUtils;
|
||||
import org.apache.hc.core5.http.io.entity.StringEntity;
|
||||
import org.apache.hc.core5.http.message.BasicNameValuePair;
|
||||
import org.apache.hc.core5.http.protocol.BasicHttpContext;
|
||||
import org.apache.hc.core5.http.protocol.HttpContext;
|
||||
import org.apache.hc.core5.net.URIBuilder;
|
||||
import org.dom4j.Document;
|
||||
import org.dom4j.Element;
|
||||
import org.dom4j.io.SAXReader;
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
import org.json.simple.parser.JSONParser;
|
||||
import org.json.simple.parser.ParseException;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import com.eactive.eai.adapter.http.HttpMemoryLogger;
|
||||
import com.eactive.eai.adapter.http.HttpMethodType;
|
||||
import com.eactive.eai.adapter.http.client.HttpClient5AdapterServiceSupport;
|
||||
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey;
|
||||
import com.eactive.eai.adapter.http.client.HttpClientAdapterVO;
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.authoutbound.AccessTokenManagerByDB;
|
||||
import com.eactive.eai.common.exception.ExceptionUtil;
|
||||
import com.eactive.eai.common.message.MessageType;
|
||||
import com.eactive.eai.common.util.CommonLib;
|
||||
import com.jayway.jsonpath.DocumentContext;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.openbanking.eai.common.token.AccessTokenManager;
|
||||
import com.openbanking.eai.common.token.AccessTokenVO;
|
||||
import com.openbanking.eai.common.token.OAuth2AccessTokenVO;
|
||||
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
|
||||
public class HttpClient5AdapterServiceKbank extends HttpClient5AdapterServiceSupport
|
||||
implements HttpClientAdapterServiceKey {
|
||||
|
||||
public static final String TYPE_VARIABLE_URL_REQUEST = "variableUrlRequest";
|
||||
public static final String TYPE_SIMPLE_REQUEST = "simpleRequest";
|
||||
public static final String REST_OPTION = "REST_OPTION";
|
||||
//public static final String HEADER_CONTENT_TYPE = "Content-Type";
|
||||
public static final String HEADER_GROUP = "HEADER_GROUP";
|
||||
public static final String HTTP_STATUS = "HTTP_STATUS";
|
||||
public static final String HEADER_KEYS = "HEADER_KEYS";
|
||||
public static final String AUTH_TOKEN = "AUTH_TOKEN";
|
||||
|
||||
/**
|
||||
* 1. 기능 : REST API 통신에 사용 <br>
|
||||
* 2. 처리 개요 : - 속성 정보를 설정 하고 수동 시스템 서비스를 호출 한다. <br>
|
||||
* 3. 주의사항 <br>
|
||||
*
|
||||
* @param prop Http Adapter 속성 정보
|
||||
* @return 반환 된 Object
|
||||
* @exception Exception 수동 시스템 간 통신 중 발생
|
||||
**/
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
public Object execute(Properties prop, Object data, Properties tempProp) throws Exception {
|
||||
HttpClientAdapterVO vo = super.setting(prop, tempProp);
|
||||
|
||||
// 2025.04.09 Authorization 헤더값을 프로퍼티(AUTH_TOKEN)에서 지정하여 설정할 수 있도록 수정
|
||||
String bizCode = "";
|
||||
String authToken = "";
|
||||
try {
|
||||
Properties authProp = PropManager.getInstance().getProperties(AUTH_TOKEN); //Authorization
|
||||
bizCode = vo.getAdapterGroupName().substring(1, 4);
|
||||
authToken = authProp.getProperty(bizCode, "");
|
||||
} catch (Exception e) {
|
||||
logger.debug("Property Group AUTH_TOKEN is not Exist !!");
|
||||
}
|
||||
|
||||
logger.debug("authToken :::::::::::::::::::::::::::::::::::" + authToken);
|
||||
|
||||
String headerGroupName = prop.getProperty(HEADER_GROUP);
|
||||
String relayResponseHeaderKeys = prop.getProperty(HEADER_KEYS, "");
|
||||
boolean useAdapterToken = StringUtils.equalsIgnoreCase(prop.getProperty("ADAPTER_TOKEN_USE_YN", "N"), "Y");
|
||||
|
||||
boolean useForwardProxy = StringUtils.equalsIgnoreCase(prop.getProperty("FORWARD_PROXY_USE_YN", "N"), "Y");
|
||||
String forwardProxyUrl = prop.getProperty("FORWARD_PROXY_URL");
|
||||
|
||||
// ex) $.dataHeader.GW_RSLT_CD
|
||||
String tokenErrorCodeKey = prop.getProperty("ADAPTER_TOKEN_ERROR_CODE_KEY");
|
||||
String tokenErrorCodeValues = prop.getProperty("ADAPTER_TOKEN_ERROR_CODE_VALUES");
|
||||
String tokenErrorHttpStatusCode = prop.getProperty("ADAPTER_TOKEN_ERROR_HTTP_STATUS_CODE"); // 200 or 400번대
|
||||
|
||||
// 레이아웃 메시지 타입 REST URL 추출 시 활용. Default JSON
|
||||
String messageType = prop.getProperty("MESSAGE_TYPE", MessageType.JSON);
|
||||
String inboundMethod = tempProp.getProperty(HttpAdapterServiceKey.INBOUND_METHOD, "POST");
|
||||
|
||||
/**
|
||||
* @formatter:off
|
||||
* TSEAIHE02.RESTOPTION 정보 => JSON 형태로 구성
|
||||
* - type : simpleRequest, variableUrlRequest
|
||||
* - extraPath : 어댑터 프로퍼티 URL에 추가될 HTTP REST URL
|
||||
* - method : get, delete, post, put
|
||||
* - contentType: application/json(default), application/x-www-form-urlencoded
|
||||
* - adapterTokenUseYn: Y, N(default)
|
||||
* ex)
|
||||
* {"type":"simpleRequest","extraPath":"v2.0/accout/balance","method":"post"}
|
||||
* @formatter:on
|
||||
*/
|
||||
|
||||
String restOptionData = tempProp.getProperty(REST_OPTION);
|
||||
|
||||
JSONObject restOptionObject = parseJson(restOptionData);
|
||||
if (restOptionObject == null) {
|
||||
throw new Exception("OptionData parsing result is NULL");
|
||||
}
|
||||
|
||||
String rmethod = (String) restOptionObject.getOrDefault("method", inboundMethod);
|
||||
String contentType = (String) restOptionObject.getOrDefault("contentType", "application/json");
|
||||
String adapterTokenUseYn = (String) restOptionObject.get("adapterTokenUseYn");
|
||||
if (StringUtils.isNotBlank(adapterTokenUseYn)) { // interface 에 설정된게 우선한다.
|
||||
useAdapterToken = StringUtils.equalsIgnoreCase(adapterTokenUseYn, "Y");
|
||||
}
|
||||
|
||||
HttpClient mclient = this.client;
|
||||
String sendData = "";
|
||||
if (data instanceof String) {
|
||||
sendData = (String) data;
|
||||
} else if (data instanceof byte[]) {
|
||||
sendData = new String((byte[]) data, vo.getEncode());
|
||||
}
|
||||
|
||||
////mclient.getParams().setContentCharset(vo.getEncode());
|
||||
|
||||
JSONObject dataObject = null;
|
||||
|
||||
Object httpHeader = null;
|
||||
Object dataContent = null;
|
||||
|
||||
if (MessageType.JSON.equals(messageType)) {
|
||||
|
||||
Object parsed = parseJsonGeneric(sendData);
|
||||
|
||||
if(parsed instanceof JSONObject) {
|
||||
dataObject = parseJson(sendData);
|
||||
if (dataObject != null && StringUtils.isNotBlank(headerGroupName)) {
|
||||
httpHeader = dataObject.get(headerGroupName);
|
||||
dataObject.remove(headerGroupName);
|
||||
}
|
||||
|
||||
dataContent = dataObject;
|
||||
if (dataObject.containsKey("innerList")) {
|
||||
JSONArray innerList = (JSONArray) dataObject.get("innerList");
|
||||
dataContent = innerList;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("HttpClientAdapterServiceRest] dataObject1 = [" + dataObject + "]");
|
||||
logger.debug("HttpClientAdapterServiceRest] dataContent = [" + dataContent + "]");
|
||||
|
||||
// interface 에 설정된게 우선한다.
|
||||
String uri = null;
|
||||
if (dataObject == null) {
|
||||
uri = changeUrl(messageType, vo.getUrl(), restOptionData, sendData);
|
||||
} else {
|
||||
uri = changeUrl(messageType, vo.getUrl(), restOptionData, dataObject);
|
||||
}
|
||||
|
||||
// ////if (vo.getConnectionTimeout() != 0) {
|
||||
// mclient.getHttpConnectionManager().getParams().setConnectionTimeout(vo.getConnectionTimeout());
|
||||
// }
|
||||
|
||||
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") URL=[" + vo.getUrl() + "] ");
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") PARAMETER_NAME=[" + vo.getParameterName() + "] ");
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") ENCODE=[" + vo.getEncode() + "] ");
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") RESPONSE_TYPE=[" + vo.getResponseType() + "] ");
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") URL_ENCODE_YN=[" + vo.getUrlEncodeYn() + "] ");
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") CONNECTION_TIMEOUT=[" + vo.getConnectionTimeout() + "] ");
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") REST_OPTION=[" + restOptionData + "] ");
|
||||
}
|
||||
|
||||
HttpUriRequestBase method = null;
|
||||
|
||||
// 메소드 타입에 따라 HttpRequestBase 인스턴스 생성
|
||||
switch (HttpMethodType.getValue(rmethod)) {
|
||||
case GET:
|
||||
method = new HttpGet(uri);
|
||||
break;
|
||||
case DELETE:
|
||||
method = new HttpDelete(uri);
|
||||
break;
|
||||
case POST:
|
||||
method = new HttpPost(uri);
|
||||
break;
|
||||
case PUT:
|
||||
method = new HttpPut(uri);
|
||||
break;
|
||||
default:
|
||||
method = new HttpGet(uri);
|
||||
break;
|
||||
}
|
||||
|
||||
RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
|
||||
|
||||
if(useForwardProxy) {
|
||||
java.net.URL url = new java.net.URL(forwardProxyUrl);
|
||||
|
||||
// 프로토콜, 호스트, 포트 추출
|
||||
String protocol = url.getProtocol();
|
||||
String host = url.getHost();
|
||||
int port = url.getPort();
|
||||
HttpHost proxy = new HttpHost(protocol,host, port);
|
||||
requestConfigBuilder.setProxy(proxy);
|
||||
}
|
||||
|
||||
configureHttpClient(requestConfigBuilder, vo, method);
|
||||
|
||||
switch (HttpMethodType.getValue(rmethod)) {
|
||||
case GET:
|
||||
case DELETE:
|
||||
HashMap<String, String> h = getParameters(dataObject);
|
||||
URIBuilder uriBuilder = new URIBuilder(uri);
|
||||
for (Map.Entry<String, String> entry : h.entrySet()) {
|
||||
uriBuilder.addParameter(entry.getKey(), entry.getValue());
|
||||
}
|
||||
URI fullUri = uriBuilder.build();
|
||||
if (method instanceof HttpGet) {
|
||||
((HttpGet) method).setUri(fullUri);
|
||||
} else if (method instanceof HttpDelete) {
|
||||
((HttpDelete) method).setUri(fullUri);
|
||||
}
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("HttpClientAdapterServiceRest] (get Method) QueryString = [" + fullUri.getQuery() + "]");
|
||||
}
|
||||
break;
|
||||
case PUT:
|
||||
case POST:
|
||||
//assignPostBody(dataObject, contentType, vo.getEncode(), method);
|
||||
assignPostBody(dataContent, contentType, vo.getEncode(), method);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") method.getParams()=["
|
||||
+ method.getEntity() + "] ");
|
||||
}
|
||||
|
||||
OAuth2AccessTokenVO accessToken = null;
|
||||
if (useAdapterToken) {
|
||||
AccessTokenManagerByDB tokenManager = AccessTokenManagerByDB.getInstance();
|
||||
accessToken = (OAuth2AccessTokenVO) tokenManager.getAccessTokenVO(vo.getAdapterGroupName());
|
||||
|
||||
// 토큰이 없거나 만료 됐으면 재발급
|
||||
if (accessToken == null || accessToken.isExpired()) {
|
||||
String oldToken = accessToken == null ? null : accessToken.getAccessToken();
|
||||
accessToken = (OAuth2AccessTokenVO) tokenManager.retryAccessTokenVO(vo.getAdapterGroupName(), prop,
|
||||
oldToken);
|
||||
}
|
||||
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") TOKEN = ["
|
||||
+ accessToken + "]");
|
||||
}
|
||||
|
||||
setAuthHeaders(method, accessToken);
|
||||
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName()
|
||||
+ ") RequestHeader [Authorization=" + method.getHeader("Authorization") + "]");
|
||||
}
|
||||
}
|
||||
|
||||
// 2025.04.09 Authorization 헤더값을 프로퍼티(AUTH_TOKEN)에서 지정하여 설정할 수 있도록 수정
|
||||
if(!StringUtils.isBlank(authToken)) {
|
||||
setAuthHeaders(method, authToken);
|
||||
}
|
||||
|
||||
// 전달 header 셋팅
|
||||
// Adapter 레벨 header 보다 data로 넘어온 httpHeader를 우선 적용(header명이 같다면 덮어씀)
|
||||
assignRequestHeaders(method, httpHeader);
|
||||
|
||||
int status = -1;
|
||||
|
||||
try {
|
||||
byte[] responseMessage = null;
|
||||
Header[] responseHeaders;
|
||||
|
||||
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
|
||||
if (dataContent != null) { //dataObject
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") = ["
|
||||
+ dataContent.toString() + "]"); //dataObject.toJSONString()
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") = "
|
||||
+ CommonLib.getDumpMessage(dataContent.toString())); //dataObject.toJSONString()
|
||||
} else {
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") = [" + sendData
|
||||
+ "]");
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName() + ") = "
|
||||
+ CommonLib.getDumpMessage(sendData));
|
||||
}
|
||||
}
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
stopWatch.start();
|
||||
|
||||
if (vo.getTraceLevel() >= 3) {
|
||||
HttpMemoryLogger.txlog(vo.getAdapterGroupName() + vo.getAdapterName(),
|
||||
"SEND [" + sendData + "]" + CommonLib.getDumpMessage(sendData));
|
||||
}
|
||||
|
||||
String uuid = tempProp.getProperty(TransactionContextKeys.TRANSACTION_UUID);
|
||||
HttpContext context = new BasicHttpContext();
|
||||
context.setAttribute(TransactionContextKeys.TRANSACTION_UUID, uuid);
|
||||
context.setAttribute(HttpClientAdapterServiceKey.ADAPTER_GROUP_NAME, vo.getAdapterGroupName());
|
||||
context.setAttribute(HttpClientAdapterServiceKey.ADAPTER_NAME, vo.getAdapterName());
|
||||
Integer logProcessNo = (Integer) tempProp.get(HttpClientAdapterServiceKey.LOG_PROCESS_NO);
|
||||
if (logProcessNo != null && logProcessNo > 0) {
|
||||
context.setAttribute(HttpClientAdapterServiceKey.LOG_PROCESS_NO, logProcessNo);
|
||||
}
|
||||
|
||||
try {
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("[method getName]" + method.getMethod());
|
||||
logger.debug("[method getEntity]" + method.getEntity());
|
||||
logger.debug("[method getRequestHeaders]" + method.getHeaders().toString());
|
||||
logger.debug("[method getURI]" + method.getPath());
|
||||
}
|
||||
try (CloseableHttpResponse response = (CloseableHttpResponse) mclient.execute(method, context)) {
|
||||
status = response.getCode();
|
||||
|
||||
if (status >= 200 && status <= 207) {
|
||||
status = 200;
|
||||
}
|
||||
|
||||
// if (status == 404) {
|
||||
// status = 200;
|
||||
// }
|
||||
|
||||
responseMessage = EntityUtils.toByteArray(response.getEntity());
|
||||
responseHeaders = response.getHeaders();
|
||||
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("[received status]" + status);
|
||||
logger.debug("HttpClientAdapterServiceRest] RECV (" + vo.getAdapterGroupName() + ") = [" + responseMessage + "]");
|
||||
}
|
||||
}
|
||||
} catch (ConnectException e) {
|
||||
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
|
||||
logger.debug("HttpClientAdapterServiceRest] responseType =" + vo.getResponseType());
|
||||
}
|
||||
throw new Exception("excuteMethod java.net.ConnectException = " + e.getMessage());
|
||||
}
|
||||
|
||||
stopWatch.stop();
|
||||
|
||||
/**
|
||||
* @formatter:off
|
||||
* OAuth 토큰 응답 체크(Adapter properties로 설정)
|
||||
* 응답코드에 따라 유효한 토큰 확인(토큰 재발급 여부 확인)
|
||||
* API 서비스 마다 정책이 다름(보통 400대에서 체크하나 200에서도 체크할 수 있음)
|
||||
*
|
||||
* TOKEN_ERROR_CODE_KEY: 응답 json error code key
|
||||
* __ex) $.dataHeader.resultCode
|
||||
* TOKEN_ERROR_CODE_VALUES: 토큰 재발급이 필요한 응답 error codes(ex: O0001,O0002,O0003)
|
||||
* TOKEN_ERROR_HTTP_STATUS_CODE: 보통 400대에서 체크하나 API 제공자에 따라 200에서도 체클할수 있음
|
||||
* ex) TOKEN_ERROR_HTTP_STATUS_CODE = 200
|
||||
* @formatter:on
|
||||
*/
|
||||
boolean needReissue = false;
|
||||
|
||||
Properties responseHeaderProp = assignRelayDataToInbound(tempProp, responseHeaders);
|
||||
String responseString = new String(responseMessage, vo.getEncode());
|
||||
|
||||
Map<String, Object> map = new HashMap<String, Object>();
|
||||
tempProp.put(HttpAdapterServiceKey.OUTBOUND_PROPERTY_MAP, map);
|
||||
map.put(HttpAdapterServiceKey.OUTBOUND_RESPONSE_HEADERS, responseHeaderProp);
|
||||
map.put(HttpAdapterServiceKey.OUTBOUND_RESPONSE_MESSAGE, responseString);
|
||||
map.put(HttpAdapterServiceKey.OUTBOUND_RESPONSE_STATUS, status);
|
||||
|
||||
if (status >= 200 && status <= 207) {
|
||||
if (useAdapterToken && StringUtils.equals(tokenErrorHttpStatusCode, "200")) {
|
||||
needReissue = checkTokenRetry(responseMessage, tokenErrorCodeKey, tokenErrorCodeValues,
|
||||
vo.getEncode());
|
||||
}
|
||||
} else if (status == 302) {
|
||||
if (!StringUtils.contains(relayResponseHeaderKeys, "Location")) {
|
||||
relayResponseHeaderKeys += ",Location";
|
||||
}
|
||||
} else {
|
||||
String errMsg = String.format(
|
||||
"http receive status fail value=%d adapterName=%s.%s uri=%s method=%s response Data=%s",
|
||||
status, vo.getAdapterGroupName(), vo.getAdapterName(), uri, rmethod, responseString);
|
||||
// logger.error(errMsg);
|
||||
|
||||
if (status >= 400 && status < 500) {
|
||||
if (useAdapterToken) {
|
||||
needReissue = checkTokenRetry(responseMessage, tokenErrorCodeKey, tokenErrorCodeValues,
|
||||
vo.getEncode());
|
||||
}
|
||||
|
||||
// if (!needReissue) {
|
||||
// throw new Exception(errMsg);
|
||||
// }
|
||||
} else {
|
||||
|
||||
throw new Exception(errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
|
||||
logger.debug("HttpClientAdapterServiceRest] responseType =" + vo.getResponseType());
|
||||
logger.debug("HttpClientAdapterServiceRest] RECV " + vo.getEncode() + "(" + vo.getAdapterGroupName() + ") = [" + new String(responseMessage, vo.getEncode()) + "]");
|
||||
// logger.debug("HttpClientAdapterServiceRest] RECV MS949 (" + vo.getAdapterGroupName() + ") = [" + new String(responseMessage, "MS949") + "]");
|
||||
// logger.debug("HttpClientAdapterServiceRest] RECV euc-kr (" + vo.getAdapterGroupName() + ") = [" + new String(responseMessage, "euc-kr") + "]");
|
||||
logger.debug("HttpClientAdapterServiceRest] RECV (" + vo.getAdapterGroupName() + ") = " + CommonLib.getDumpMessage(responseMessage));
|
||||
}
|
||||
|
||||
if (useAdapterToken) {
|
||||
if (responseMessage == null) {
|
||||
throw new Exception("responseMessage is NULL, HttpStatus=" + status);
|
||||
}
|
||||
|
||||
// OAuth 토큰 재요청 코드 확인
|
||||
if (needReissue) {
|
||||
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
|
||||
logger.debug("HttpClientAdapterServiceRest] retry access token response code = ["
|
||||
+ vo.getResponseType() + "] message = [" + new String(responseMessage, vo.getEncode())
|
||||
+ "]");
|
||||
}
|
||||
|
||||
// 토큰 재발급
|
||||
AccessTokenManager tokenManager = AccessTokenManager.getInstance();
|
||||
String oldToken = accessToken == null ? null : accessToken.getAccessToken();
|
||||
OAuth2AccessTokenVO newaccessToken = (OAuth2AccessTokenVO) tokenManager
|
||||
.retryAccessTokenVO(vo.getAdapterGroupName(), prop, oldToken);
|
||||
method.setHeader("Authorization", newaccessToken.getAuthorization());
|
||||
|
||||
setAuthHeaders(method, newaccessToken);
|
||||
|
||||
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
|
||||
logger.debug("HttpClientAdapterServiceRest] SEND (" + vo.getAdapterGroupName()
|
||||
+ ") RETRY TOKEN = [" + newaccessToken + "]");
|
||||
}
|
||||
|
||||
try (CloseableHttpResponse response = (CloseableHttpResponse) mclient.execute(method)) {
|
||||
status = response.getCode();
|
||||
responseMessage = EntityUtils.toByteArray(response.getEntity());
|
||||
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("[received status]" + status);
|
||||
logger.debug("HttpClientAdapterServiceRest] RECV (" + vo.getAdapterGroupName() + ") = [" + new String(responseMessage, vo.getEncode()) + "]");
|
||||
}
|
||||
}catch (IOException e){
|
||||
throw new Exception("retry excuteMethod Exceptioin = " + e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
if (status == 302) {
|
||||
if (!StringUtils.contains(relayResponseHeaderKeys, "Location")) {
|
||||
relayResponseHeaderKeys += ",Location";
|
||||
}
|
||||
} else {
|
||||
String errMsg = String.format(
|
||||
"http receive status fail value=%d adapterName=%s.%s uri=%s method=%s response Data=%s",
|
||||
status, vo.getAdapterGroupName(), vo.getAdapterName(), uri, rmethod, responseMessage);
|
||||
throw new Exception(errMsg);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
if (logger.isDebug() && "N".equals(vo.getTestCallYn())) {
|
||||
logger.debug("HttpClientAdapterServiceRest] RETRY responseType =" + vo.getResponseType());
|
||||
logger.debug("HttpClientAdapterServiceRest] RETRY RECV " + vo.getEncode() + "("
|
||||
+ vo.getAdapterGroupName() + ") = [" + new String(responseMessage, vo.getEncode())
|
||||
+ "]");
|
||||
logger.debug("HttpClientAdapterServiceRest] RETRY RECV (" + vo.getAdapterGroupName() + ") = "
|
||||
+ CommonLib.getDumpMessage(responseMessage));
|
||||
}
|
||||
// ----------------------------------------------------------
|
||||
}
|
||||
}
|
||||
|
||||
if (vo.getTraceLevel() >= 3) {
|
||||
HttpMemoryLogger.txlog(vo.getAdapterGroupName() + vo.getAdapterName(),
|
||||
"RECV [" + new String(responseMessage, vo.getEncode()) + "]"
|
||||
+ CommonLib.getDumpMessage(responseMessage));
|
||||
}
|
||||
|
||||
|
||||
return assignResponseHeaders(method, responseMessage, headerGroupName, vo.getEncode(), messageType,
|
||||
relayResponseHeaderKeys, status, responseHeaderProp);
|
||||
} catch (SocketTimeoutException ste) { // Read Timeout :: HttpClient 3.1일 경우
|
||||
if (vo.getTraceLevel() >= 3) {
|
||||
HttpMemoryLogger.error(vo.getAdapterGroupName() + vo.getAdapterName(),
|
||||
"HttpClientAdapterServiceRest] SocketTimeoutException (" + vo.getAdapterGroupName() + ") : "
|
||||
+ ste.toString(),
|
||||
ste);
|
||||
}
|
||||
logger.error("HttpClientAdapterServiceRest] SocketTimeoutException (" + vo.getAdapterGroupName() + ") : "
|
||||
+ ste.toString(), ste);
|
||||
throw ste;
|
||||
} catch (ConnectException ce) {
|
||||
if (vo.getTraceLevel() >= 3) {
|
||||
HttpMemoryLogger.error(vo.getAdapterGroupName() + vo.getAdapterName(),
|
||||
"HttpClientAdapterServiceRest] Connection Exception (" + vo.getAdapterGroupName() + ") : "
|
||||
+ ce.toString(),
|
||||
ce);
|
||||
}
|
||||
logger.error("HttpClientAdapterServiceRest] Connection Exception (" + vo.getAdapterGroupName() + ") : "
|
||||
+ ce.toString(), ce);
|
||||
throw ce;
|
||||
} catch (Exception e) {
|
||||
if (vo.getTraceLevel() >= 3) {
|
||||
HttpMemoryLogger.error(vo.getAdapterGroupName() + vo.getAdapterName(), e.toString(), e);
|
||||
}
|
||||
logger.error("HttpClientAdapterServiceRest] Exception (" + vo.getAdapterGroupName() + ") : " + e.toString(),
|
||||
e);
|
||||
throw e;
|
||||
} finally {
|
||||
if (status != HttpStatus.SC_OK) {
|
||||
String[] msgArgs = new String[1];
|
||||
msgArgs[0] = String.valueOf(status);
|
||||
String resMsg = ExceptionUtil.make("RDCEAIAHA013", msgArgs);
|
||||
if (logger.isDebug()) {
|
||||
logger.debug(resMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Properties assignRelayDataToInbound(Properties prop, Header[] responseHeaders) {
|
||||
Properties headerProp = new Properties();
|
||||
if (responseHeaders == null || responseHeaders.length == 0) {
|
||||
return headerProp;
|
||||
}
|
||||
|
||||
for (Header header : responseHeaders) {
|
||||
headerProp.put(header.getName(), header.getValue());
|
||||
}
|
||||
|
||||
return headerProp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 1단계 레이아웃에만 적용되도록 구현됨.
|
||||
*
|
||||
* @param method
|
||||
* @param responseMessage
|
||||
* @param headerGroupName
|
||||
* @param encode
|
||||
* @param messageType
|
||||
* @param relayHeaderKeys
|
||||
* @return
|
||||
* @throws Exception
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private String assignResponseHeaders(HttpUriRequestBase method, byte[] responseMessage, String headerGroupName,
|
||||
String encode, String messageType, String relayHeaderKeys, int status, Properties responseHeaderProp) throws Exception {
|
||||
if (!MessageType.JSON.equals(messageType) || StringUtils.isBlank(headerGroupName)
|
||||
|| StringUtils.isBlank(relayHeaderKeys)) {
|
||||
return new String(responseMessage, encode);
|
||||
}
|
||||
|
||||
String[] relayKeyArr = org.springframework.util.StringUtils.tokenizeToStringArray(relayHeaderKeys, ",");
|
||||
JSONObject headerJson = new JSONObject();
|
||||
for (String key : relayKeyArr) {
|
||||
String value = responseHeaderProp.getProperty(key);
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
headerJson.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
headerJson.put(HTTP_STATUS, String.valueOf(status));
|
||||
|
||||
if (headerJson.size() <= 0) {
|
||||
return new String(responseMessage, encode);
|
||||
}
|
||||
|
||||
JSONObject message = null;
|
||||
if (responseMessage == null || responseMessage.length == 0) {
|
||||
message = new JSONObject();
|
||||
} else {
|
||||
message = parseJson(new String(responseMessage, encode));
|
||||
if (message == null) {
|
||||
message = new JSONObject();
|
||||
message.put("Malformed_Response_Message", new String(responseMessage, encode));
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtils.isNotBlank(headerGroupName) && message != null)
|
||||
message.put(headerGroupName, headerJson);
|
||||
|
||||
logger.info("------------- message --------------- : {} ", message);
|
||||
|
||||
return message.toJSONString();
|
||||
}
|
||||
|
||||
// 2025.04.09 Authorization 헤더값을 프로퍼티(AUTH_TOKEN)에서 지정하여 설정할 수 있도록 수정
|
||||
private void setAuthHeaders(HttpUriRequestBase method, String authorization) throws Exception {
|
||||
method.setHeader("Authorization",
|
||||
String.format("%s", authorization));
|
||||
}
|
||||
|
||||
private void setAuthHeaders(HttpUriRequestBase method, OAuth2AccessTokenVO accessToken) throws Exception {
|
||||
method.setHeader("Authorization",
|
||||
String.format("%s %s", AccessTokenVO.BEARER_TYPE, accessToken.getAccessToken()));
|
||||
}
|
||||
|
||||
private boolean checkTokenRetry(byte[] responseMessage, String tokenErrorCodeKey, String tokenErrorCodeValues,
|
||||
String encode) {
|
||||
if (responseMessage == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(tokenErrorCodeKey)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (StringUtils.isBlank(tokenErrorCodeValues)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
DocumentContext jsonContext = JsonPath.parse(new String(responseMessage, encode));
|
||||
String responseCode = jsonContext.read(tokenErrorCodeKey);
|
||||
if (StringUtils.isBlank(responseCode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String[] arr = org.springframework.util.StringUtils.tokenizeToStringArray(tokenErrorCodeValues, ",");
|
||||
return ArrayUtils.contains(arr, responseCode);
|
||||
} catch (Exception e) {
|
||||
logger.error("HttpClientAdapterServiceRest] checkTokenRetry error=" + e.getMessage());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void assignRequestHeaders(HttpUriRequestBase method, Object httpHeader) {
|
||||
if (httpHeader == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (httpHeader instanceof JSONObject) {
|
||||
JSONObject jsonObject = (JSONObject) httpHeader;
|
||||
for (Object key : jsonObject.keySet()) {
|
||||
Object obj = jsonObject.get(key);
|
||||
if ((obj instanceof JSONObject) || (obj instanceof JSONArray)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
method.setHeader((String) key, (String) obj);
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Request Header :" + (String) key + "=[" + (String) obj + "]");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void assignPostBody(Object eaiBody, String contentType, String charset, HttpUriRequestBase method) {
|
||||
if (eaiBody == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (StringUtils.contains(contentType, "application/x-www-form-urlencoded")) {
|
||||
if (eaiBody instanceof JSONObject) {
|
||||
List<NameValuePair> params = new ArrayList<>();
|
||||
JSONObject jsonObject = (JSONObject) eaiBody;
|
||||
for (Object key : jsonObject.keySet()) {
|
||||
params.add(new BasicNameValuePair((String) key, (String) jsonObject.get(key)));
|
||||
}
|
||||
|
||||
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(params, StandardCharsets.UTF_8);
|
||||
method.setEntity(entity);
|
||||
}
|
||||
|
||||
} else {
|
||||
ContentType contentTypeObj = ContentType.create(contentType, charset);
|
||||
|
||||
// 요청 본문을 StringEntity 객체로 생성
|
||||
StringEntity entity = new StringEntity(eaiBody.toString(), contentTypeObj);
|
||||
|
||||
// 요청에 본문 추가
|
||||
method.setEntity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
private HashMap<String, String> getParameters(Object message) throws Exception {
|
||||
|
||||
HashMap<String, String> result = new HashMap<String, String>();
|
||||
|
||||
if (message != null) {
|
||||
if (message instanceof JSONObject) {
|
||||
JSONObject jsonObject = (JSONObject) message;
|
||||
for (Object key : jsonObject.keySet()) {
|
||||
Object obj = jsonObject.get(key);
|
||||
if ((obj instanceof JSONObject) || (obj instanceof JSONArray)) {
|
||||
continue;
|
||||
}
|
||||
result.put((String) key, getStringValue(obj));
|
||||
}
|
||||
} else {
|
||||
String[] messages = ((String) message).split("&");
|
||||
String[] data = null;
|
||||
|
||||
for (int i = 0; i < messages.length; i++) {
|
||||
data = messages[i].split("=", 2);
|
||||
if (data.length == 2) {
|
||||
result.put(data[0], data[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public String getStringValue(Object obj) {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (obj instanceof String) {
|
||||
return (String) obj;
|
||||
} else if (obj instanceof Long) {
|
||||
return Long.toString((Long) obj);
|
||||
} else if (obj instanceof BigDecimal) {
|
||||
return ((BigDecimal) obj).toPlainString();
|
||||
} else {
|
||||
return (String) obj;
|
||||
}
|
||||
}
|
||||
|
||||
public static Object parseJsonGeneric(String jsonData) {
|
||||
if (StringUtils.isBlank(jsonData)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
JSONParser parser = new JSONParser();
|
||||
Object parsedObj = parser.parse(jsonData);
|
||||
return parsedObj;
|
||||
} catch (ParseException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private JSONObject parseJson(String message) throws Exception {
|
||||
if (message == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (JSONObject) JSONValue.parse(message);
|
||||
}
|
||||
|
||||
private Document convertXmlDocument(String message) throws Exception {
|
||||
SAXReader builder = new SAXReader();
|
||||
Document document = builder.read(new StringReader(message));
|
||||
return document;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private String changeUrl(String messageType, String url, String restOption, Object sendData) {
|
||||
if ((MessageType.JSON.equals(messageType) || MessageType.XML.equals(messageType))) {
|
||||
try {
|
||||
if (StringUtils.isBlank(restOption)) {
|
||||
return url;
|
||||
}
|
||||
List<String> urlVariableList = new ArrayList<String>();
|
||||
JSONObject restOptionObject = parseJson(restOption);
|
||||
|
||||
if (restOptionObject == null)
|
||||
throw new Exception("restOption is NULL");
|
||||
|
||||
String type = (String) restOptionObject.get("type");
|
||||
String requestExtraPath = (String) restOptionObject.get("extraPath");
|
||||
url = getUrl(url, requestExtraPath);
|
||||
if (TYPE_VARIABLE_URL_REQUEST.equals(type)) {
|
||||
Pattern p = Pattern.compile("\\{(.*?)\\}");
|
||||
Matcher m = p.matcher(requestExtraPath);
|
||||
List<String> uriVariables = new ArrayList<>();
|
||||
while(m.find()) {
|
||||
String fieldName = m.group(1);
|
||||
uriVariables.add(fieldName);
|
||||
}
|
||||
if (MessageType.JSON.equals(messageType)) {
|
||||
JSONObject jsonObject;
|
||||
if (sendData instanceof JSONObject) {
|
||||
jsonObject = (JSONObject) sendData;
|
||||
} else {
|
||||
jsonObject = (JSONObject) JSONValue.parse((String) sendData);
|
||||
}
|
||||
for (Object tempObject : uriVariables) {
|
||||
String urlVaribleId = (String) tempObject;
|
||||
String uriVariable = (String) jsonObject.get(urlVaribleId);
|
||||
urlVariableList.add(uriVariable);
|
||||
jsonObject.remove(urlVaribleId);
|
||||
}
|
||||
} else if (MessageType.XML.equals(messageType)) {
|
||||
Document doc;
|
||||
if (sendData instanceof Document) {
|
||||
doc = (Document) sendData;
|
||||
} else {
|
||||
doc = convertXmlDocument((String) sendData);
|
||||
}
|
||||
for (Object tempObject : uriVariables) {
|
||||
String urlVaribleId = (String) tempObject;
|
||||
Element element = (Element) doc.selectSingleNode("//" + urlVaribleId);
|
||||
urlVariableList.add(element.getText());
|
||||
if (doc.getRootElement() == element) {
|
||||
doc = null;
|
||||
} else {
|
||||
element.getParent().remove(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (urlVariableList.size() > 0) {
|
||||
UriComponents uriComponents = UriComponentsBuilder.fromUriString(url).build();
|
||||
Object[] urls = urlVariableList.toArray();
|
||||
url = uriComponents.expand(urls).toUriString();
|
||||
logger.debug("HttpClientAdapterServiceRest] after extand url=[" + url + "] ");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to change url", e);
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
private String getUrl(String baseUrl, String extraPath) {
|
||||
if (StringUtils.isBlank(extraPath)) {
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
if (StringUtils.contains(extraPath, "http://") || StringUtils.contains(extraPath, "https://")) {
|
||||
return extraPath;
|
||||
}
|
||||
|
||||
String targetUrl = baseUrl;
|
||||
if (!targetUrl.endsWith("/")) {
|
||||
targetUrl += "/";
|
||||
}
|
||||
if (extraPath.startsWith("/")) {
|
||||
targetUrl += extraPath.substring(1);
|
||||
} else {
|
||||
targetUrl += extraPath;
|
||||
}
|
||||
|
||||
logger.debug("HttpClientAdapterServiceRest] after concatenate url=[" + targetUrl + "] ");
|
||||
return targetUrl;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -85,11 +85,4 @@ public interface HttpAdapterServiceKey {
|
||||
|
||||
//응답 처리 용 표준 전문 오브젝트
|
||||
static final String STANDARD_MESSAGE_OBJECT = "STANDARD_MESSAGE_OBJECT";
|
||||
|
||||
//송신(Outbound) 요청 표준 전문 오브젝트
|
||||
static final String OUT_REQ_STD_MSG = "OUT_REQ_STD_MSG";
|
||||
|
||||
// 어댑터별 인증 키 헤더 이름
|
||||
static final String ADAPTER_TOKEN_HEADER_NAME = "ADAPTER_TOKEN_HEADER_NAME";
|
||||
static final String ADAPTER_APIKEY_HEADER_NAME = "ADAPTER_APIKEY_HEADER_NAME";
|
||||
}
|
||||
|
||||
@@ -6,116 +6,51 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
import com.eactive.eai.adapter.AdapterGroupVO;
|
||||
import com.eactive.eai.adapter.AdapterManager;
|
||||
import com.eactive.eai.adapter.RequestDispatcher;
|
||||
import com.eactive.eai.adapter.http.HttpStatusException;
|
||||
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilterFactory;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilterFactoryKjb;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilterType;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.server.EAIServerManager;
|
||||
import com.eactive.eai.common.util.JacksonUtil;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.common.util.TxSiftContext;
|
||||
import com.eactive.eai.common.util.UUIDGenerator;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public abstract class HttpAdapterServiceSupport implements HttpAdapterService, HttpAdapterServiceKey {
|
||||
private static final String LOG_PREFIX = "HttpAdapterServiceSupport] ";
|
||||
protected long slowTranTime = 2000L;
|
||||
public static final String HEADER_NAME_CLIENT_ID = "x-elink-client-id";
|
||||
public static final String PROPERTIES_NAME_CLIENT_ID = "clientId";
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
// 기본 ObjectMapper 는 JsonNode 직렬화 시 작은 소수를 1.2E-7 로 바꿔버린다.
|
||||
private ObjectMapper mapper = JacksonUtil.newNumberSafeMapper();
|
||||
EAIServerManager eaiServerManager;
|
||||
String instid = null;
|
||||
|
||||
|
||||
public Object service(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
String uuid = prop.getProperty(TransactionContextKeys.TRANSACTION_UUID);
|
||||
boolean bMDCput = false;
|
||||
|
||||
if(uuid == null) {
|
||||
if(instid == null) {
|
||||
eaiServerManager = EAIServerManager.getInstance();
|
||||
instid = eaiServerManager.getGroupInstId();
|
||||
}
|
||||
uuid = instid + UUIDGenerator.getUUID().toString().replaceAll("-", "");
|
||||
prop.setProperty(TransactionContextKeys.TRANSACTION_UUID, uuid);
|
||||
}
|
||||
|
||||
bMDCput = TxSiftContext.begin(uuid);
|
||||
|
||||
try {
|
||||
String clientId = request.getHeader(HEADER_NAME_CLIENT_ID);
|
||||
if (StringUtils.isNotBlank(clientId)) {
|
||||
prop.put(PROPERTIES_NAME_CLIENT_ID, clientId);
|
||||
}
|
||||
|
||||
String transactionId = request.getHeader(HttpClientAdapterServiceKey.TRANSACTION_ID);
|
||||
if (StringUtils.isNotBlank(transactionId)) {
|
||||
prop.put(HttpClientAdapterServiceKey.TRANSACTION_ID, transactionId);
|
||||
}
|
||||
|
||||
try {
|
||||
String clientId = request.getHeader(HEADER_NAME_CLIENT_ID);
|
||||
if (StringUtils.isNotBlank(clientId)) {
|
||||
prop.put(PROPERTIES_NAME_CLIENT_ID, clientId);
|
||||
}
|
||||
|
||||
String instanceId = request.getHeader(HttpClientAdapterServiceKey.INSTANCE_ID);
|
||||
if (StringUtils.isNotBlank(instanceId)) {
|
||||
prop.put(HttpClientAdapterServiceKey.INSTANCE_ID, instanceId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn(LOG_PREFIX + "Failed to read request headers: " + e.getMessage());
|
||||
String instanceId = request.getHeader(HttpClientAdapterServiceKey.INSTANCE_ID);
|
||||
if (StringUtils.isNotBlank(instanceId)) {
|
||||
prop.put(HttpClientAdapterServiceKey.INSTANCE_ID, instanceId);
|
||||
}
|
||||
|
||||
Object obj = null;
|
||||
try {
|
||||
message = doPreFilters(adptGrpName, adptName, message, prop, request, response);
|
||||
|
||||
if (message instanceof JSONObject) {
|
||||
message = ((JSONObject) message).toJSONString();
|
||||
} else if (message instanceof JsonNode) {
|
||||
message = mapper.writeValueAsString((JsonNode) message);
|
||||
}
|
||||
|
||||
obj = RequestDispatcher.getRequestDispatcher(adptGrpName).handle(adptName, message, prop);
|
||||
obj = doPostFilters(adptGrpName, adptName, obj, prop, request, response);
|
||||
} catch (HttpStatusException e) { // inbound error
|
||||
logger.warn(LOG_PREFIX + transactionId + "-" + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
|
||||
throw e;
|
||||
} catch (JwtAuthException e) { // filter error
|
||||
logger.error(LOG_PREFIX + transactionId + "-" + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
|
||||
// UnkownMessageLogUtils.logUnkownMessage(adptGrpName, adptName, message, prop, request, response,
|
||||
// "RECEAIIRP202", e);
|
||||
throw e;
|
||||
} catch (Exception e) { // filter error
|
||||
logger.error(LOG_PREFIX + transactionId + "-" + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
|
||||
// UnkownMessageLogUtils.logUnkownMessage(adptGrpName, adptName, message, prop, request, response,
|
||||
// "RECEAIIRP201", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (obj == null) {
|
||||
return null;
|
||||
} else if (obj instanceof byte[]) {
|
||||
return (byte[]) obj;
|
||||
} else if (obj instanceof String) {
|
||||
return (String) obj;
|
||||
} else if (obj instanceof JSONObject) {
|
||||
return ((JSONObject) obj).toJSONString();
|
||||
} else if (obj instanceof JsonNode) {
|
||||
return (JsonNode) obj;
|
||||
} else {
|
||||
throw new Exception("RECEAIAHA001");
|
||||
}
|
||||
} finally {
|
||||
if(bMDCput)
|
||||
TxSiftContext.end();
|
||||
} catch (Exception e) {
|
||||
}
|
||||
|
||||
message = doPreFilters(adptGrpName, adptName, message, prop, request, response);
|
||||
Object obj = RequestDispatcher.getRequestDispatcher(adptGrpName).handle(adptName, message, prop);
|
||||
obj = doPostFilters(adptGrpName, adptName, obj, prop, request, response);
|
||||
if (obj == null) {
|
||||
return null;
|
||||
} else if (obj instanceof byte[]) {
|
||||
return (byte[]) obj;
|
||||
} else if (obj instanceof String) {
|
||||
return (String) obj;
|
||||
} else {
|
||||
throw new Exception("RECEAIAHA001");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +62,7 @@ public abstract class HttpAdapterServiceSupport implements HttpAdapterService, H
|
||||
if (!"HTC".equals(group.getType())) {
|
||||
String allowIp = prop.getProperty(ALLOW_IP,"");
|
||||
if (StringUtils.isNotBlank(allowIp)) {
|
||||
HttpAdapterFilter adapterFilter = HttpAdapterFilterFactory.createFilter(HttpAdapterFilterType.ADAPTERALLOWIPLISTFILTER.toString());
|
||||
HttpAdapterFilter adapterFilter = HttpAdapterFilterFactoryKjb.createFilter(HttpAdapterFilterType.ADAPTERALLOWIPLISTFILTER.toString());
|
||||
if (adapterFilter != null) {
|
||||
message = adapterFilter.doPreFilter(adptGrpName, adptName, message, prop, request, response);
|
||||
}
|
||||
@@ -145,7 +80,7 @@ public abstract class HttpAdapterServiceSupport implements HttpAdapterService, H
|
||||
continue;
|
||||
}
|
||||
|
||||
HttpAdapterFilter adapterFilter = HttpAdapterFilterFactory.createFilter(filterName.trim());
|
||||
HttpAdapterFilter adapterFilter = HttpAdapterFilterFactoryKjb.createFilter(filterName.trim());
|
||||
if (adapterFilter != null) {
|
||||
message = adapterFilter.doPreFilter(adptGrpName, adptName, message, prop, request, response);
|
||||
} else {
|
||||
@@ -168,7 +103,7 @@ public abstract class HttpAdapterServiceSupport implements HttpAdapterService, H
|
||||
continue;
|
||||
}
|
||||
|
||||
HttpAdapterFilter adapterFilter = HttpAdapterFilterFactory.createFilter(filterName.trim());
|
||||
HttpAdapterFilter adapterFilter = HttpAdapterFilterFactoryKjb.createFilter(filterName.trim());
|
||||
if (adapterFilter != null) {
|
||||
resultMessage = adapterFilter.doPostFilter(adptGrpName, adptName, resultMessage, prop, request,
|
||||
response);
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.dynamic;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.adapter.AdapterGroupVO;
|
||||
import com.eactive.eai.adapter.AdapterManager;
|
||||
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey;
|
||||
import com.eactive.eai.common.exception.ExceptionUtil;
|
||||
import com.eactive.eai.common.server.EAIServerManager;
|
||||
import com.eactive.eai.common.util.DatetimeUtil;
|
||||
import com.eactive.eai.common.util.InboundErrorLogger;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.common.util.UUIDGenerator;
|
||||
import com.eactive.eai.inbound.error.InboundErrorInfoVO;
|
||||
import com.eactive.eai.inbound.error.InboundErrorKeys;
|
||||
import com.eactive.eai.util.HexaConverter;
|
||||
|
||||
public class UnknownMessageLogUtils {
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
|
||||
|
||||
private UnknownMessageLogUtils() {
|
||||
throw new IllegalStateException("Utility class");
|
||||
}
|
||||
|
||||
public static void logUnkownMessage(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response, String errCode, Exception e) {
|
||||
try {
|
||||
EAIServerManager eaiServerManager = EAIServerManager.getInstance();
|
||||
String serverName = eaiServerManager.getLocalServerName();
|
||||
String txId = prop.getProperty(HttpClientAdapterServiceKey.TRANSACTION_ID);
|
||||
if(StringUtils.isEmpty(txId)) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
|
||||
String instanceid1 = serverName.substring(0,2);
|
||||
String instanceid2 = serverName.substring(serverName.length()-2, serverName.length());
|
||||
String instid = instanceid1 + instanceid2;
|
||||
txId = instid + UUIDGenerator.getUUID();
|
||||
}
|
||||
String clientId = prop.getProperty("clientId");
|
||||
String hexClientId = clientId != null ? HexaConverter.bytesToHexa(clientId.getBytes()) : "";
|
||||
String errorMsg = ExceptionUtil.make(e, errCode);
|
||||
StringBuffer sb = new StringBuffer();
|
||||
sb.append(errorMsg).append("\n").append("Remote Addr : ").append(request.getRemoteAddr()).append("\n").append("Request URI : ")
|
||||
.append(request.getRequestURI()).append("\n").append("clientId=").append(clientId).append(",hexClientId=").append(hexClientId).append(",txProp=").append(prop).append("\n").append("Exception : ").append(e.getMessage());
|
||||
UnknownMessageLogUtils.logUnknownMessage(txId, adptGrpName, adptName,
|
||||
"", "", false, errCode, sb.toString(), System.currentTimeMillis(),
|
||||
serverName, InboundErrorKeys.IN_UNKNOWN, message);
|
||||
} catch (Exception ex) {
|
||||
logger.warn("unkown log fail.", ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// public static void logUnkownMessage(String uuid, String adapterGroupName, String adapterName, String errCode,
|
||||
// String errMsg, Object message) {
|
||||
// UnkownMessageLogUtils.logUnknownMessage(uuid, adapterGroupName, adapterName,
|
||||
// "", "", false, errCode, errMsg, System.currentTimeMillis(),
|
||||
// EAIServerManager.getInstance().getInstId(), InboundErrorKeys.IN_UNKNOWN, message);
|
||||
// }
|
||||
|
||||
private static void logUnknownMessage(String uuid, String adapterGroupName, String adapterName,
|
||||
String bzwkSvcKeyName, String eaiSvcCd, boolean isTasStarted, String errCode, String errMsg, long errTm,
|
||||
String svcInstNm, String errDstCd, Object message) {
|
||||
String msg = null;
|
||||
|
||||
if(errDstCd == null || errDstCd.length() ==0) {
|
||||
errDstCd = "ND";
|
||||
}
|
||||
|
||||
if(isTasStarted) {
|
||||
errDstCd = InboundErrorKeys.TAS_START;
|
||||
}
|
||||
|
||||
InboundErrorInfoVO errorInfoVO = new InboundErrorInfoVO();
|
||||
errorInfoVO.setEaiSvcSno(uuid); // EAI서비스일련번호
|
||||
errorInfoVO.setAdptBwkGrpNm(adapterGroupName); // 어댑터업무그룹명
|
||||
errorInfoVO.setEaiSvcCd(eaiSvcCd);
|
||||
errorInfoVO.setErrCd(errCode); // 에러코드
|
||||
errorInfoVO.setErrTxt(errMsg); // 에러내용
|
||||
errorInfoVO.setErrTm(DatetimeUtil.getCurrentTime(errTm)); // 에러발생시각
|
||||
errorInfoVO.setErrDstcd(errDstCd);
|
||||
errorInfoVO.setBzwkSvcKeyName(bzwkSvcKeyName);
|
||||
|
||||
if(message == null) {
|
||||
msg = "null";
|
||||
}
|
||||
else {
|
||||
if(message instanceof byte[]) {
|
||||
AdapterManager adapterManager = AdapterManager.getInstance();
|
||||
AdapterGroupVO srcAdapterGrpVO = adapterManager.getAdapterGroupVO(adapterGroupName);
|
||||
|
||||
// TAS 거래일 경우 NULL일 수 있음
|
||||
if(srcAdapterGrpVO == null) {
|
||||
msg = new String((byte[])message);
|
||||
}
|
||||
else {
|
||||
// String srcAdapterMsgType = srcAdapterGrpVO.getMessageType();
|
||||
// if( MessageUtil.isUTF8(srcAdapterMsgType) ) {
|
||||
// try {
|
||||
// msg = new String((byte[])message, UJSONMessage.encode);
|
||||
// } catch (UnsupportedEncodingException e) {
|
||||
// logger.warn("msg encode error", e);
|
||||
// msg = new String((byte[])message); }
|
||||
// }
|
||||
// else {
|
||||
// msg = new String((byte[])message);
|
||||
// }
|
||||
|
||||
String charset = StringUtils.defaultIfBlank(srcAdapterGrpVO.getMessageEncode(), Charset.defaultCharset().name());
|
||||
try {
|
||||
msg = new String((byte[])message, charset);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
logger.warn("msg encode error", e);
|
||||
msg = new String((byte[])message);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else if(message instanceof String) {
|
||||
msg = (String)message;
|
||||
}
|
||||
else {
|
||||
msg = "invaild message ["+message.getClass().getName()+"] "+message.toString();
|
||||
}
|
||||
}
|
||||
|
||||
errorInfoVO.setBwkDataTxt(msg); // 업무데이터내용
|
||||
errorInfoVO.setEaiSvrInstNm(svcInstNm); // EAI서버인스턴스명
|
||||
|
||||
InboundErrorLogger.error(errorInfoVO);
|
||||
|
||||
if (logger.isError()) {
|
||||
logger.error("======================================================================" );
|
||||
logger.error(" RequestDispatcher] GET UNKNOWN MESSAGE");
|
||||
logger.error(" ADAPTER GROUP NAME [" + adapterGroupName + "]");
|
||||
logger.error(" EAISVCNAME [" + eaiSvcCd + "]");
|
||||
logger.error(" ADAPTER NAME [" + adapterName + "]");
|
||||
logger.error("======================================================================" );
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+12
-10
@@ -1,18 +1,20 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import com.eactive.eai.adapter.http.HttpStatusException;
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.util.IpUtil;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Properties;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class AdapterAllowIPListFilter implements HttpAdapterFilter {
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
@@ -28,9 +30,9 @@ public class AdapterAllowIPListFilter implements HttpAdapterFilter {
|
||||
}
|
||||
|
||||
if (!IpUtil.isMatchIp(allowedIps, requestIp)) {
|
||||
throw new FilterException("This IP was not allowed-" + requestIp, HttpStatus.FORBIDDEN.value());
|
||||
throw new HttpStatusException("This IP was not allowed-" + requestIp, HttpStatus.FORBIDDEN.value());
|
||||
}
|
||||
} catch (FilterException e) {
|
||||
} catch (HttpStatusException e) {
|
||||
logger.debug(e.getMessage());
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
|
||||
+7
-5
@@ -8,12 +8,14 @@ import javax.servlet.http.HttpServletResponse;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import com.eactive.eai.adapter.http.HttpStatusException;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.util.IpUtil;
|
||||
|
||||
|
||||
public class AdapterBlockIpListFilter implements HttpAdapterFilter {
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
@@ -26,9 +28,9 @@ public class AdapterBlockIpListFilter implements HttpAdapterFilter {
|
||||
}
|
||||
|
||||
if (IpUtil.isMatchIp(blockIps, requestIp)) {
|
||||
throw new FilterException("This IP was not allowed-" + requestIp, HttpStatus.FORBIDDEN.value());
|
||||
throw new HttpStatusException("This IP was not allowed-" + requestIp, HttpStatus.FORBIDDEN.value());
|
||||
}
|
||||
} catch (FilterException e) {
|
||||
} catch (HttpStatusException e) {
|
||||
logger.debug(e.getMessage());
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
@@ -44,9 +46,9 @@ public class AdapterBlockIpListFilter implements HttpAdapterFilter {
|
||||
if (ipAddress == null) {
|
||||
ipAddress = request.getRemoteAddr();
|
||||
}
|
||||
if (ipAddress.indexOf(",") >= 0) {
|
||||
if(ipAddress.indexOf(",") >= 0) {
|
||||
ipAddress = org.springframework.util.StringUtils.tokenizeToStringArray(ipAddress, ",")[0];
|
||||
}
|
||||
}
|
||||
return ipAddress;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,5 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.text.ParseException;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.collections.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport;
|
||||
import com.eactive.eai.authserver.service.BearerTokenInfo;
|
||||
//import com.eactive.eai.authserver.vo.BearerTokenInfo;
|
||||
@@ -34,6 +13,7 @@ import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.session.SessionManager;
|
||||
import com.eactive.eai.common.stdmessage.STDMessageManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.common.util.StringUtil;
|
||||
import com.eactive.eai.inbound.action.ActionFactory;
|
||||
import com.eactive.eai.inbound.action.RequestAction;
|
||||
import com.eactive.eai.inbound.processor.Processor;
|
||||
@@ -44,13 +24,31 @@ import com.nimbusds.jose.JWSVerifier;
|
||||
import com.nimbusds.jose.crypto.RSASSAVerifier;
|
||||
import com.nimbusds.jose.util.IOUtils;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.text.ParseException;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class ApiAuthFilter implements HttpAdapterFilter {
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String PROP_GROUP_AUTH_SERVER = "OAuthServer";
|
||||
public static final String PROP_KEYSTORE_PATH = "certification.publicKeyPath";
|
||||
public static final String PASS_SCOPE_LIST = "pass.scope.list";
|
||||
|
||||
public static final String ERROR_AUTHENTICATION_FAIL = "E.AUTHENTICATION_FAIL";
|
||||
public static final String ERROR_AUTHORIZATION_FAIL = "E.AUTHORIZATION_FAIL";
|
||||
@@ -60,7 +58,7 @@ public class ApiAuthFilter implements HttpAdapterFilter {
|
||||
public static final String PAYLOAD_PARAM_NAME_CLIENT_ID = "client_id";
|
||||
|
||||
private JWSVerifier jwsVerifier;
|
||||
private String[] passScopeArr = {};
|
||||
|
||||
// // CA 토큰 저장소
|
||||
// private final Map<String, BearerTokenInfo> CATokenStore = new ConcurrentHashMap<>();
|
||||
|
||||
@@ -90,34 +88,23 @@ public class ApiAuthFilter implements HttpAdapterFilter {
|
||||
} catch (IOException | NoSuchAlgorithmException | RuntimeException | InvalidKeySpecException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
String passScope = vo.getProperty(PASS_SCOPE_LIST, "oob,public");
|
||||
passScopeArr = org.springframework.util.StringUtils.tokenizeToStringArray(passScope, ",");
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
String apiId = prop.getProperty("FINAL_STD_MESSAGE_KEY");
|
||||
String eaiSvcCd = prop.getProperty("API_SERVICE_CODE");
|
||||
if(StringUtils.isEmpty(eaiSvcCd)) {
|
||||
if(StringUtils.isEmpty(apiId))
|
||||
apiId = assignApiId(adptGrpName, adptName, message, prop, request);
|
||||
|
||||
if (StringUtils.isBlank(apiId)) {
|
||||
throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Can not find apiId(apiCode) info");
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: 중복되는 표준메시지 조회로 리소드 낭비가 있을 수 있으므로 체크 자체를 추후 RequestProcessor혹은 RestProcessor로 이동 필요 해 보임
|
||||
* ApiRequestProcessor을 만드는것도 괜찮을 수 있을 것 같으나 관리가 안될 것 같음
|
||||
*/
|
||||
StandardMessage standardMessage = STDMessageManager.getInstance().getSTDMessage(apiId);
|
||||
eaiSvcCd = StandardMessageManager.getInstance().getMapper().getEaiSvcCode(standardMessage);
|
||||
String apiId = assignApiId(adptGrpName, adptName, message, prop, request);
|
||||
if (StringUtils.isBlank(apiId)) {
|
||||
throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Can not find apiId(apiCode) info");
|
||||
}
|
||||
// if (StringUtils.isNotEmpty(eaiSvcCd) && !StringUtils.equals(eaiSvcCd, ))
|
||||
// eaiSvcCd = prop.getProperty("API_SERVICE_CODE");
|
||||
|
||||
/**
|
||||
* TODO: 중복되는 표준메시지 조회로 리소드 낭비가 있을 수 있으므로 체크 자체를 추후 RequestProcessor혹은 RestProcessor로 이동 필요 해 보임
|
||||
* ApiRequestProcessor을 만드는것도 괜찮을 수 있을 것 같으나 관리가 안될 것 같음
|
||||
*/
|
||||
StandardMessage standardMessage = STDMessageManager.getInstance().getSTDMessage(apiId);
|
||||
String eaiSvcCd = StandardMessageManager.getInstance().getMapper().getEaiSvcCode(standardMessage);
|
||||
if(!StringUtils.equals(eaiSvcCd, prop.getProperty("API_SERVICE_CODE"))) eaiSvcCd = prop.getProperty("API_SERVICE_CODE");
|
||||
EAIMessage eaiMessage = EAIMessageManager.getInstance().getEAIMessage(eaiSvcCd);
|
||||
|
||||
if(StringUtils.isBlank(eaiMessage.getAuthType())){
|
||||
@@ -128,7 +115,7 @@ public class ApiAuthFilter implements HttpAdapterFilter {
|
||||
switch (eaiMessage.getAuthType()){
|
||||
case "oauth":
|
||||
try {
|
||||
String token = extractBearerToken(request, prop);
|
||||
String token = extractBearerToken(request);
|
||||
SignedJWT signedJWT = SignedJWT.parse(token);
|
||||
|
||||
if (signedJWT.getJWTClaimsSet().getExpirationTime() == null) {
|
||||
@@ -147,32 +134,26 @@ public class ApiAuthFilter implements HttpAdapterFilter {
|
||||
|
||||
OAuth2Manager manager = OAuth2Manager.getInstance();
|
||||
HashSet<String> scopeSet = manager.getApiScopeMap().get(apiId);
|
||||
|
||||
String[] scopeArr = signedJWT.getJWTClaimsSet().getStringArrayClaim(PAYLOAD_PARAM_NAME_SCOPE);
|
||||
if (scopeArr == null || scopeArr.length == 0) {
|
||||
throw new JwtAuthException(ERROR_AUTHORIZATION_FAIL,
|
||||
String.format("Insufficient [Scope](Allowed Scope=%s)", scopeSet.toString()));
|
||||
}
|
||||
|
||||
// API에 scope 설정이 안되어 있는 경우, scope 체크를 pass하고 나중에 client API 맵을 체크한다.
|
||||
if(CollectionUtils.isEmpty(scopeSet))
|
||||
isPassScope = true;
|
||||
|
||||
if(!isPassScope) {
|
||||
String[] scopeArr = signedJWT.getJWTClaimsSet().getStringArrayClaim(PAYLOAD_PARAM_NAME_SCOPE);
|
||||
if (scopeArr == null || scopeArr.length == 0) {
|
||||
throw new JwtAuthException(ERROR_AUTHORIZATION_FAIL,
|
||||
String.format("Insufficient [Scope](Allowed Scope=%s)", scopeSet.toString()));
|
||||
}
|
||||
|
||||
for (String scope : scopeArr) {
|
||||
if (StringUtils.equals(scope, "oob") || StringUtils.equals(scope, "public")) {
|
||||
isPassScope = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPassScope) {
|
||||
if (!verifyScope(scopeSet, signedJWT)) {
|
||||
throw new JwtAuthException(ERROR_AUTHORIZATION_FAIL,
|
||||
String.format("Insufficient [Scope](Allowed Scope=%s)", scopeSet.toString()));
|
||||
}
|
||||
for (String scope : scopeArr) {
|
||||
if (StringUtils.equals(scope, "oob") || StringUtils.equals(scope, "public")) {
|
||||
isPassScope = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isPassScope) {
|
||||
if (!verifyScope(scopeSet, signedJWT)) {
|
||||
throw new JwtAuthException(ERROR_AUTHORIZATION_FAIL,
|
||||
String.format("Insufficient [Scope](Allowed Scope=%s)", scopeSet.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
// header 값으로 전송된 clientId와 토큰 소유 clientId check
|
||||
if (!verifyClientId(prop, signedJWT)) {
|
||||
throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "client_id not matched(header/token)");
|
||||
@@ -186,23 +167,8 @@ public class ApiAuthFilter implements HttpAdapterFilter {
|
||||
}
|
||||
break;
|
||||
case "api_key":
|
||||
// 기관별로 API Key 헤더명이 다를 수 있어, 어댑터별 설정(ADAPTER_TOKEN_HEADER_NAME)을 우선 사용하고
|
||||
// 없으면 ApiConfig 전역 설정(api.key.name)을 사용. 콤마로 다중 헤더명 지정 시 순서대로 값이 있는 헤더를 사용
|
||||
String apiKeyNameConf = prop.getProperty(HttpAdapterServiceKey.ADAPTER_APIKEY_HEADER_NAME);
|
||||
if (StringUtils.isBlank(apiKeyNameConf)) {
|
||||
apiKeyNameConf = PropManager.getInstance().getProperty("ApiConfig", "api.key.name", "x-api-key");
|
||||
}
|
||||
String[] apiKeyNames = org.springframework.util.StringUtils.tokenizeToStringArray(apiKeyNameConf, ",");
|
||||
String apiKeyName = null;
|
||||
String apiKey = null;
|
||||
for (String name : apiKeyNames) {
|
||||
String value = request.getHeader(name);
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
apiKeyName = name;
|
||||
apiKey = value.trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
String apiKeyName = PropManager.getInstance().getProperty("ApiConfig", "api.key.name", "x-api-key");
|
||||
String apiKey = request.getHeader(apiKeyName);
|
||||
if(apiKey == null){
|
||||
// QueryString으로 전달된 access_token 파라미터 확인
|
||||
String apiKeyParamName = PropManager.getInstance().getProperty("ApiConfig", "api.key.param.name", "x-api-key");
|
||||
@@ -210,14 +176,14 @@ public class ApiAuthFilter implements HttpAdapterFilter {
|
||||
if (StringUtils.isNotBlank(queryApiKey)) {
|
||||
apiKey = queryApiKey.trim();
|
||||
} else {
|
||||
throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Invalid or missing API key \""+apiKeyNameConf+"\" in Http Header");
|
||||
throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Invalid or missing API key \""+apiKeyName+"\" in Http Header");
|
||||
}
|
||||
}
|
||||
prop.setProperty(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID, apiKey);
|
||||
isPassScope = true;
|
||||
break;
|
||||
case "ca":
|
||||
String token = extractBearerToken(request, prop);
|
||||
String token = extractBearerToken(request);
|
||||
BearerTokenInfo bearerTokenInfo = SessionManager.getInstance().getCAToken(token);
|
||||
if ( bearerTokenInfo != null ) {
|
||||
if( bearerTokenInfo.isExpired() ) {
|
||||
@@ -321,6 +287,9 @@ public class ApiAuthFilter implements HttpAdapterFilter {
|
||||
return false;
|
||||
}
|
||||
|
||||
String passScope = "oob,public";
|
||||
String[] passScopeArr = org.springframework.util.StringUtils.tokenizeToStringArray(passScope, ",");
|
||||
|
||||
for (String scope : scopeArr) {
|
||||
for (String pScope : passScopeArr) {
|
||||
if ( StringUtils.equalsAny(scope, pScope)) {
|
||||
@@ -335,14 +304,8 @@ public class ApiAuthFilter implements HttpAdapterFilter {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static String extractBearerToken(HttpServletRequest request, Properties prop) throws JwtAuthException {
|
||||
// 기관별로 토큰 헤더명이 다를 수 있어, 어댑터별 설정(ADAPTER_APPKEY_HEADER_NAME)을 우선 사용하고
|
||||
// 없으면 ApiConfig 전역 설정(token.header.name)을 사용
|
||||
String tokenHeaderName = prop != null ? prop.getProperty(HttpAdapterServiceKey.ADAPTER_TOKEN_HEADER_NAME) : null;
|
||||
if (StringUtils.isBlank(tokenHeaderName)) {
|
||||
tokenHeaderName = PropManager.getInstance().getProperty("ApiConfig", "token.header.name", "Authorization");
|
||||
}
|
||||
String authorization = request.getHeader(tokenHeaderName);
|
||||
public static String extractBearerToken(HttpServletRequest request) throws JwtAuthException {
|
||||
String authorization = request.getHeader("Authorization");
|
||||
if (StringUtils.isBlank(authorization)) {
|
||||
// QueryString으로 전달된 access_token 파라미터 확인
|
||||
String tokenParamName = PropManager.getInstance().getProperty("ApiConfig", "token.param.name", "access_token");
|
||||
@@ -350,13 +313,13 @@ public class ApiAuthFilter implements HttpAdapterFilter {
|
||||
if (StringUtils.isNotBlank(queryToken)) {
|
||||
return queryToken.trim();
|
||||
}
|
||||
throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "No have header info: " + tokenHeaderName);
|
||||
throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "No have header info: Authorization");
|
||||
}
|
||||
|
||||
String[] components = authorization.trim().split("\\s+", 2);
|
||||
String[] components = authorization.split("\\s");
|
||||
|
||||
if (components.length != 2) {
|
||||
throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Malformat [" + tokenHeaderName + "] content");
|
||||
throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Malformat [Authorization] content");
|
||||
}
|
||||
|
||||
if (!StringUtils.equalsIgnoreCase(components[0], "Bearer")) {
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
|
||||
import com.eactive.eai.common.message.EAIMessage;
|
||||
import com.eactive.eai.common.message.EAIMessageManager;
|
||||
import com.eactive.eai.common.stdmessage.STDMessageManager;
|
||||
import com.eactive.eai.common.stdmessage.STDMsgInfoAddOnVO;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.common.util.MessageUtil;
|
||||
import com.eactive.eai.inbound.action.ActionFactory;
|
||||
import com.eactive.eai.inbound.action.RequestAction;
|
||||
import com.eactive.eai.inbound.processor.Processor;
|
||||
import com.eactive.eai.message.StandardMessage;
|
||||
import com.eactive.eai.message.manager.StandardMessageManager;
|
||||
import com.eactive.eai.message.service.InterfaceMapper;
|
||||
|
||||
/**
|
||||
* 요청 url를 기반으로 interface id 찾기
|
||||
* tx prop에 STD_MESSAGE_KEY, FINAL_STD_MESSAGE_KEY, INTERFACE_ID, EAI_MESSAGE 세팅
|
||||
*/
|
||||
public class ApiKeyExtractFilter implements HttpAdapterFilter {
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String STD_MESSAGE_KEY = "STD_MESSAGE_KEY";
|
||||
public static final String FINAL_STD_MESSAGE_KEY = "FINAL_STD_MESSAGE_KEY";
|
||||
public static final String API_SERVICE_CODE = "API_SERVICE_CODE";
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
|
||||
String actionName = prop.getProperty(Processor.REQUEST_ACTION);
|
||||
RequestAction action = ActionFactory.createAction(actionName);
|
||||
action.setAdapterInfo(adptGrpName, adptName, prop);
|
||||
String[] keys = action.perform(message);
|
||||
String requestPath = keys[0];
|
||||
|
||||
prop.setProperty(STD_MESSAGE_KEY, requestPath);// action class 통해서 생성
|
||||
|
||||
// PathVariable 지원 추가
|
||||
String ruledPath = getMatchedKey(requestPath);
|
||||
if(ruledPath == null)
|
||||
throw new FilterException("path not found - " + requestPath, ERROR_PRE_FAIL, HttpStatus.FORBIDDEN.value());
|
||||
prop.setProperty(FINAL_STD_MESSAGE_KEY, ruledPath);// StandardMessageUtil.getMatchedKey(), url
|
||||
// pathvariable도 처리
|
||||
|
||||
String apiId = prop.getProperty(ApiKeyExtractFilter.FINAL_STD_MESSAGE_KEY);
|
||||
if (StringUtils.isBlank(apiId)) {
|
||||
throw new JwtAuthException(MessageUtil.ERROR_CODE_AUTH_FAIL, "Can not find apiId(apiCode) info");
|
||||
}
|
||||
|
||||
String apiSvcCode = this.getEaiSvcCode(apiId);
|
||||
prop.put(API_SERVICE_CODE, apiSvcCode);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private String getEaiSvcCode(String apiId) throws Exception {
|
||||
StandardMessage standardMessage = STDMessageManager.getInstance().getSTDMessage(apiId);
|
||||
return StandardMessageManager.getInstance().getMapper().getEaiSvcCode(standardMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
return resultMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* API 서비스에서 실제 요청된 path를 설정된 PathVariable 형태의 key를 찾는다
|
||||
* ex) /api/bank/account/12345 --> /api/bank/account/{accountNo}
|
||||
* @param key
|
||||
* @param actionName
|
||||
* @return
|
||||
*/
|
||||
public static String getMatchedKey(String key) {
|
||||
STDMessageManager stdMessageManager = STDMessageManager.getInstance();
|
||||
String[] keys = stdMessageManager.getAllSTDMessageKeys();
|
||||
for (String keyStr : keys) {
|
||||
if (StringUtils.equals(key, keyStr)) {
|
||||
return keyStr;
|
||||
}
|
||||
}
|
||||
|
||||
return stdMessageManager.getMatchedPathVariable(key);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class DebugLoggerInFilter implements HttpAdapterFilter {
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
logger.debug("Inbound Filter RCV adptGrpName : {}", adptGrpName);
|
||||
logger.debug("Inbound Filter RCV adptName : {}", adptName);
|
||||
logger.debug("Inbound Filter RCV message : {}", message);
|
||||
logger.debug("Inbound Filter RCV prop : {}", prop);
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
logger.debug("Inbound Filter SND adptGrpName : {}", adptGrpName);
|
||||
logger.debug("Inbound Filter SND adptName : {}", adptName);
|
||||
logger.debug("Inbound Filter SND resultMessage : {}", resultMessage);
|
||||
logger.debug("Inbound Filter SND prop : {}", prop);
|
||||
return resultMessage;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public class FilterCryptoException extends FilterException {
|
||||
public FilterCryptoException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
public FilterCryptoException(String msg, int status) {
|
||||
super(msg, status);
|
||||
}
|
||||
public FilterCryptoException(String msg, String code, int status) {
|
||||
super(msg, code, status);
|
||||
}
|
||||
|
||||
public FilterCryptoException(String msg, String code, int status, Throwable cause) {
|
||||
super(msg, code, status, cause);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter;
|
||||
|
||||
import com.eactive.eai.adapter.http.HttpStatusException;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public class FilterException extends HttpStatusException {
|
||||
public FilterException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
public FilterException(String msg, int status) {
|
||||
super(msg, status);
|
||||
}
|
||||
public FilterException(String msg, String code, int status) {
|
||||
super(msg, code, status);
|
||||
}
|
||||
|
||||
public FilterException(String msg, String code, int status, Throwable cause) {
|
||||
super(msg, code, status, cause);
|
||||
}
|
||||
|
||||
}
|
||||
+5
-5
@@ -28,7 +28,7 @@ import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class HmacSha256VerifyFilterKjb implements HttpAdapterFilter {
|
||||
|
||||
static final String DJB_ROOTLESS_ARRAY = "{ \"DJB_ROOTLESS_ARRAY\" : ";
|
||||
static final String KJB_ROOTLESS_ARRAY = "{ \"KJB_ROOTLESS_ARRAY\" : ";
|
||||
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
@@ -63,8 +63,8 @@ public class HmacSha256VerifyFilterKjb implements HttpAdapterFilter {
|
||||
String chkBody = inboundBody;
|
||||
String clientSecret = assignClientSecret(prop, request);
|
||||
|
||||
if (chkBody.startsWith(DJB_ROOTLESS_ARRAY)) {
|
||||
chkBody = chkBody.substring(DJB_ROOTLESS_ARRAY.length());
|
||||
if (chkBody.startsWith(KJB_ROOTLESS_ARRAY)) {
|
||||
chkBody = chkBody.substring(KJB_ROOTLESS_ARRAY.length());
|
||||
|
||||
if (chkBody.endsWith("}")) {
|
||||
chkBody = chkBody.substring(0, chkBody.length() - 1);
|
||||
@@ -161,8 +161,8 @@ public class HmacSha256VerifyFilterKjb implements HttpAdapterFilter {
|
||||
|
||||
Object root = JSONValue.parse(outboundBody);
|
||||
|
||||
if (root instanceof JSONObject && ((JSONObject) root).containsKey("DJB_ROOTLESS_ARRAY")) {
|
||||
JSONArray jsonArray = (JSONArray) ((JSONObject) root).get("DJB_ROOTLESS_ARRAY");
|
||||
if (root instanceof JSONObject && ((JSONObject) root).containsKey("KJB_ROOTLESS_ARRAY")) {
|
||||
JSONArray jsonArray = (JSONArray) ((JSONObject) root).get("KJB_ROOTLESS_ARRAY");
|
||||
outboundBody = jsonArray.toJSONString();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,6 @@ import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
public interface HttpAdapterFilter {
|
||||
|
||||
public static final String ERROR_PRE_FAIL = "E.PREFILTER_FAIL";
|
||||
public static final String ERROR_POST_FAIL = "E.POSTFILTER_FAIL";
|
||||
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception;
|
||||
|
||||
|
||||
+10
-24
@@ -6,10 +6,13 @@ import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class HttpAdapterFilterFactory {
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
static String basePackage = "com.eactive.eai.adapter.http.dynamic.filter";
|
||||
static String customBasePackage = "com.eactive.eai.custom.adapter.http.dynamic.filter";
|
||||
static private ConcurrentHashMap<String, HttpAdapterFilter> h = new ConcurrentHashMap<String, HttpAdapterFilter>();
|
||||
|
||||
public HttpAdapterFilterFactory() {
|
||||
super();
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public static HttpAdapterFilter createFilter(String type) {
|
||||
if (h.containsKey(type)) {
|
||||
return (HttpAdapterFilter) h.get(type);
|
||||
@@ -29,19 +32,12 @@ public class HttpAdapterFilterFactory {
|
||||
case ADAPTERALLOWIPLISTFILTER:
|
||||
filter = new AdapterAllowIPListFilter();
|
||||
break;
|
||||
case HMAC_SHA256:
|
||||
filter = new HmacSha256VerifyFilterKjb();
|
||||
break;
|
||||
case REFLECTALLHEADER:
|
||||
filter = new ReflectAllHeaderFilter();
|
||||
break;
|
||||
default:
|
||||
filter = classForName(type);
|
||||
if (filter == null) {
|
||||
filter = classForName(basePackage + "." + type);
|
||||
}
|
||||
if (filter == null) {
|
||||
filter = classForName(customBasePackage + "." + type);
|
||||
try {
|
||||
Class cl = Class.forName(type);
|
||||
filter = (HttpAdapterFilter) cl.newInstance();
|
||||
} catch (Exception e) {
|
||||
logger.error("Cannot create a HttpAdapterFilter. - {}", type);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -53,14 +49,4 @@ public class HttpAdapterFilterFactory {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpAdapterFilter classForName(String type) {
|
||||
try {
|
||||
Class<?> cl = Class.forName(type);
|
||||
return (HttpAdapterFilter) cl.newInstance();
|
||||
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
|
||||
logger.error("Cannot create a HttpAdapterFilter. - {}", type);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class HttpAdapterFilterFactoryKjb extends HttpAdapterFilterFactory {
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
static String basePackage = "com.eactive.eai.custom.adapter.http.dynamic.filter";
|
||||
static private ConcurrentHashMap<String, HttpAdapterFilter> h = new ConcurrentHashMap<String, HttpAdapterFilter>();
|
||||
|
||||
public HttpAdapterFilterFactoryKjb() {
|
||||
super();
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
public static HttpAdapterFilter createFilter(String type) {
|
||||
if (h.containsKey(type)) {
|
||||
return (HttpAdapterFilter) h.get(type);
|
||||
}
|
||||
|
||||
HttpAdapterFilter filter = null;
|
||||
switch (HttpAdapterFilterType.getValue(type)) {
|
||||
case APIAUTHFILTER:
|
||||
filter = new ApiAuthFilter();
|
||||
break;
|
||||
case JWTAUTHFILTER:
|
||||
filter = new JwtAuthFilter();
|
||||
break;
|
||||
case IPWHITELISTFILTER:
|
||||
filter = new IpWhiteListFilter();
|
||||
break;
|
||||
case ADAPTERALLOWIPLISTFILTER:
|
||||
filter = new AdapterAllowIPListFilter();
|
||||
break;
|
||||
case HMAC_SHA256:
|
||||
filter = new HmacSha256VerifyFilterKjb();
|
||||
break;
|
||||
case REFLECTALLHEADER:
|
||||
filter = new ReflectAllHeaderFilter();
|
||||
break;
|
||||
default:
|
||||
filter = classForName(type);
|
||||
if (filter == null) {
|
||||
filter = classForName(basePackage + "." + type);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (filter != null) {
|
||||
h.put(type, filter);
|
||||
return filter;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpAdapterFilter classForName(String type) {
|
||||
try {
|
||||
Class<?> cl = Class.forName(type);
|
||||
return (HttpAdapterFilter) cl.newInstance();
|
||||
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
|
||||
logger.error("Cannot create a HttpAdapterFilter. - {}", type);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.adapter.http.filter.AbstractCryptoFilter;
|
||||
|
||||
/**
|
||||
* HTTP 서버 어댑터용 암복호화 필터 (수신 방향).
|
||||
*
|
||||
* doPreFilter : 수신 요청 복호화 (CRYPTO_DEC_* 프로퍼티 기반)
|
||||
* doPostFilter : 송신 응답 암호화 (CRYPTO_ENC_* 프로퍼티 기반)
|
||||
*
|
||||
* DYNAMIC 키 컨텍스트가 필요한 경우 {@link #buildRuntimeContext}를 오버라이드한다.
|
||||
* STATIC 키 모듈을 사용하는 경우 빈 Map이 기본값이므로 오버라이드 불필요.
|
||||
*
|
||||
* 프로퍼티 설명은 {@link AbstractCryptoFilter} 참조.
|
||||
*/
|
||||
public class InCryptoFilter extends AbstractCryptoFilter implements HttpAdapterFilter {
|
||||
|
||||
public static final String ERROR_DEC_FAIL = "E.DECRYPT_FAIL";
|
||||
public static final String ERROR_ENC_FAIL = "E.ENCRYPT_FAIL";
|
||||
/**
|
||||
* DYNAMIC 키 도출용 컨텍스트를 구성한다.
|
||||
* STATIC 키 모듈이면 빈 Map({@code new HashMap<>()})을 반환한다.
|
||||
* 서브클래스에서 오버라이드하여 HttpServletRequest로부터 값을 추출할 수 있다.
|
||||
*/
|
||||
protected Map<String, String> buildRuntimeContext(Properties prop, HttpServletRequest request) {
|
||||
Map<String, String> context = new HashMap<>();
|
||||
for (String key : prop.stringPropertyNames()) {
|
||||
context.put(key, prop.getProperty(key));
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* CRYPTO_AAD_HEADER 프로퍼티에 지정된 헤더값을 UTF-8 바이트로 반환한다.
|
||||
* 미설정 또는 헤더값 없으면 null을 반환하여 GCM 기본 동작(IV를 AAD 대체값으로 사용)을 따른다.
|
||||
*/
|
||||
protected byte[] buildAad(Properties prop, HttpServletRequest request) {
|
||||
String headerName = prop.getProperty(PROP_AAD_HEADER);
|
||||
if (StringUtils.isBlank(headerName) || request == null) {
|
||||
return null;
|
||||
}
|
||||
Properties inboundHeaderProp = (Properties)prop.get("INBOUND_HEADER");
|
||||
String headerValue = inboundHeaderProp.getProperty(headerName);
|
||||
// String headerValue = request.getHeader(headerName);
|
||||
return StringUtils.isBlank(headerValue) ? null : headerValue.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
|
||||
String moduleName = required(prop, PROP_MODULE_NAME);
|
||||
String scope = getProp(prop, PROP_SCOPE, SCOPE_FIELD).toUpperCase();
|
||||
String body = toBodyString(message);
|
||||
Map<String, String> runtimeCtx = buildRuntimeContext(prop, request);
|
||||
byte[] aad = buildAad(prop, request);
|
||||
|
||||
if (SCOPE_FIELD.equals(scope)) {
|
||||
return decryptField(body, moduleName, runtimeCtx, aad,
|
||||
getProp(prop, PROP_DEC_FROM_PATH, PATH_ENCDATA),
|
||||
getProp(prop, PROP_DEC_TO_PATH, PATH_ROOT));
|
||||
}
|
||||
return decryptBody(body, moduleName, runtimeCtx, aad);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
|
||||
String moduleName = required(prop, PROP_MODULE_NAME);
|
||||
String scope = getProp(prop, PROP_SCOPE, SCOPE_FIELD).toUpperCase();
|
||||
String body = toBodyString(resultMessage);
|
||||
Map<String, String> runtimeCtx = buildRuntimeContext(prop, request);
|
||||
byte[] aad = buildAad(prop, request);
|
||||
|
||||
if (SCOPE_FIELD.equals(scope)) {
|
||||
return encryptField(body, moduleName, runtimeCtx, aad,
|
||||
getProp(prop, PROP_ENC_FROM_PATH, PATH_ROOT),
|
||||
getProp(prop, PROP_ENC_TO_PATH, PATH_ENCDATA));
|
||||
}
|
||||
return encryptBody(body, moduleName, runtimeCtx, aad);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter;
|
||||
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.util.JsonPathUtil;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
public class JsonToSetStatusFilter implements HttpAdapterFilter {
|
||||
private static final String PROPGROUP = "JsonToSetStatusFilter";
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
/** HTTP 상태코드 유효 범위 (RFC 7231) */
|
||||
private static final int MIN_HTTP_STATUS = 100;
|
||||
private static final int MAX_HTTP_STATUS = 599;
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
// do nothing
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
String fieldName = "";
|
||||
String statusParam = "";
|
||||
try {
|
||||
JsonNode rootNode = parseJson(adptGrpName, resultMessage);
|
||||
fieldName = getFieldName(adptGrpName);
|
||||
if (rootNode != null && StringUtils.isNotBlank(fieldName) && rootNode.has(fieldName)) {
|
||||
statusParam = rootNode.get(fieldName).asText();
|
||||
int httpStatus = Integer.parseInt(statusParam);
|
||||
if (isValidHttpStatus(httpStatus)) {
|
||||
response.setStatus(httpStatus);
|
||||
} else {
|
||||
logger.warn("유효하지 않은 HTTP 상태코드. 상태코드를 설정하지 않음. fieldName={}, value={}", fieldName, statusParam);
|
||||
}
|
||||
} else {
|
||||
logger.warn("설정과 맞지 않는 메시지. 상태코드를 설정하지 않음. fieldName={}", fieldName);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("상태코드 추출 실패. fieldName={}, value={}", fieldName, statusParam, e);
|
||||
}
|
||||
|
||||
return resultMessage;
|
||||
}
|
||||
|
||||
/** HTTP 상태코드로 사용 가능한 값인지 확인한다. (100 ~ 599) */
|
||||
private boolean isValidHttpStatus(int httpStatus) {
|
||||
return httpStatus >= MIN_HTTP_STATUS && httpStatus <= MAX_HTTP_STATUS;
|
||||
}
|
||||
|
||||
private String getFieldName(String adptGrpName) {
|
||||
return PropManager.getInstance().getProperty(PROPGROUP, adptGrpName);
|
||||
}
|
||||
|
||||
private JsonNode parseJson(String adptGrpName, Object message) throws Exception {
|
||||
return JsonPathUtil.toTree(message);
|
||||
}
|
||||
}
|
||||
+7
-39
@@ -51,6 +51,13 @@ public class ReflectAllHeaderFilter implements HttpAdapterFilter {
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
|
||||
logger.debug("doPreFilter ReflectAllHeaderFilter Start.");
|
||||
|
||||
String propValue = PropManager.getInstance().getProperty(PROPERTIES_GROUP_NAME, HEADER_KEY_NAMES, "").trim();
|
||||
@@ -82,45 +89,6 @@ public class ReflectAllHeaderFilter implements HttpAdapterFilter {
|
||||
}
|
||||
|
||||
logger.debug("doPreFilter ReflectAllHeaderFilter End.");
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
|
||||
logger.debug("doPostFilter ReflectAllHeaderFilter Start.");
|
||||
|
||||
String propValue = PropManager.getInstance().getProperty(PROPERTIES_GROUP_NAME, HEADER_KEY_NAMES, "").trim();
|
||||
String[] userSettingBlackList = propValue.split(",");
|
||||
|
||||
if( userSettingBlackList.length > 0 ) {
|
||||
HttpHeaderRelayBlackList = Stream
|
||||
.concat(Arrays.stream(HttpHeaderRelayBlackList), Arrays.stream(userSettingBlackList))
|
||||
.toArray(String[]::new);
|
||||
}
|
||||
|
||||
try {
|
||||
Enumeration<String> headerNames = request.getHeaderNames();
|
||||
while (headerNames.hasMoreElements()) {
|
||||
|
||||
String headerName = headerNames.nextElement();
|
||||
String headerValue = request.getHeader(headerName);
|
||||
|
||||
if( StringUtils.equalsAnyIgnoreCase( headerName, HttpHeaderRelayBlackList ) ) {
|
||||
logger.debug("Skip Processing Key ["+headerName+"], value ["+headerValue+"] in HttpHeaderRelayBlackList");
|
||||
continue;
|
||||
}
|
||||
|
||||
response.setHeader(headerName, headerValue);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
logger.debug("doPostFilter ReflectAllHeaderFilter End.");
|
||||
|
||||
return resultMessage;
|
||||
}
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
/**
|
||||
* 수신 Http Header 중 <b>허용 목록(white list)에 등록된 헤더만</b> 응답 헤더로 복사하는 Inbound Adapter Filter.
|
||||
*
|
||||
* <p>모든 헤더를 복사하고 제외 목록으로 걸러내는 {@link ReflectAllHeaderFilter} 와 달리,
|
||||
* 복사 대상을 어댑터 그룹별로 명시하는 방식이다.
|
||||
*
|
||||
* <p>설정은 <code>HttpHeaderFilter</code> property 그룹에 등록하며, 아래 순서로 가장 먼저 찾은 키 하나만 사용한다.
|
||||
* (합집합이 아니라 override 이므로 어댑터 그룹 키를 지정하면 전역 키는 무시된다.)
|
||||
*
|
||||
* <pre>
|
||||
* 1. ReflectHeaderFilter.whiteList.{어댑터그룹명} 어댑터 그룹 단위
|
||||
* 2. ReflectHeaderFilter.whiteList 전역 기본값
|
||||
* </pre>
|
||||
*
|
||||
* <p>값은 콤마로 구분한 헤더명 목록이며 대소문자를 구분하지 않는다.
|
||||
* 헤더명 끝에 <code>*</code> 를 붙이면 접두사 일치로 처리한다.
|
||||
*
|
||||
* <pre>
|
||||
* HttpHeaderFilter.ReflectHeaderFilter.whiteList = x-elink-client-id
|
||||
* HttpHeaderFilter.ReflectHeaderFilter.whiteList.djbTrans = x-obp-txid, x-obp-partnercode, X-KKB-*
|
||||
* </pre>
|
||||
*
|
||||
* <p>설정이 없거나 비어 있으면 어떤 헤더도 복사하지 않는다.
|
||||
*/
|
||||
public class ReflectHeaderFilter implements HttpAdapterFilter {
|
||||
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String PROPERTIES_GROUP_NAME = "HttpHeaderFilter";
|
||||
public static final String HEADER_KEY_NAMES = "ReflectHeaderFilter.whiteList";
|
||||
|
||||
/**
|
||||
* 허용 목록에 등록되어 있어도 복사하지 않는 헤더.
|
||||
* 요청측 값이 응답 본문/커넥션과 불일치하면 응답 자체가 깨지므로 설정으로 열 수 없게 한다.
|
||||
*/
|
||||
private static final Set<String> NEVER_REFLECT = createHeaderSet(
|
||||
"Content-Length",
|
||||
"Transfer-Encoding",
|
||||
"Connection",
|
||||
"Keep-Alive",
|
||||
"Upgrade",
|
||||
"TE",
|
||||
"Trailer");
|
||||
|
||||
/** 어댑터 그룹별 허용 목록 캐시. 필터 인스턴스는 HttpAdapterFilterFactory 에서 싱글톤으로 공유된다. */
|
||||
private final ConcurrentHashMap<String, CachedWhiteList> whiteListCache = new ConcurrentHashMap<String, CachedWhiteList>();
|
||||
|
||||
public ReflectHeaderFilter() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
logger.debug("doPreFilter ReflectHeaderFilter Start.");
|
||||
|
||||
reflectHeaders(adptGrpName, request, response);
|
||||
|
||||
logger.debug("doPreFilter ReflectHeaderFilter End.");
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
logger.debug("doPostFilter ReflectHeaderFilter Start.");
|
||||
|
||||
reflectHeaders(adptGrpName, request, response);
|
||||
|
||||
logger.debug("doPostFilter ReflectHeaderFilter End.");
|
||||
|
||||
return resultMessage;
|
||||
}
|
||||
|
||||
private void reflectHeaders(String adptGrpName, HttpServletRequest request, HttpServletResponse response) {
|
||||
|
||||
WhiteList whiteList = getWhiteList(adptGrpName);
|
||||
|
||||
if (whiteList.isEmpty()) {
|
||||
logger.debug("No reflect header configured for adapter group [" + adptGrpName + "]. Skip all.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
for (String headerName : whiteList.getNames()) {
|
||||
|
||||
String headerValue = request.getHeader(headerName);
|
||||
if (headerValue == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
setHeader(headerName, headerValue, response);
|
||||
}
|
||||
|
||||
// 접두사(*) 설정이 있을 때만 수신 헤더를 순회한다.
|
||||
if (whiteList.hasPrefix()) {
|
||||
java.util.Enumeration<String> headerNames = request.getHeaderNames();
|
||||
while (headerNames != null && headerNames.hasMoreElements()) {
|
||||
|
||||
String headerName = headerNames.nextElement();
|
||||
if (!whiteList.matchesPrefix(headerName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
setHeader(headerName, request.getHeader(headerName), response);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void setHeader(String headerName, String headerValue, HttpServletResponse response) {
|
||||
|
||||
if (NEVER_REFLECT.contains(headerName)) {
|
||||
logger.debug("Skip Processing Key [" + headerName + "] in NEVER_REFLECT list");
|
||||
return;
|
||||
}
|
||||
|
||||
// 헤더명/값에 CR, LF 가 있으면 응답 분할(response splitting) 위험이 있다.
|
||||
if (containsCrLf(headerName) || containsCrLf(headerValue)) {
|
||||
logger.error("Skip Processing Key [" + headerName + "] - CR/LF detected in header name or value");
|
||||
return;
|
||||
}
|
||||
|
||||
response.setHeader(headerName, headerValue);
|
||||
logger.debug("Processing Key [" + headerName + "], value [" + headerValue + "]");
|
||||
}
|
||||
|
||||
private boolean containsCrLf(String value) {
|
||||
return value != null && (value.indexOf('\r') >= 0 || value.indexOf('\n') >= 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 어댑터 그룹 → 전역 순으로 property 를 찾아 허용 목록을 반환한다.
|
||||
* property 값이 바뀌지 않는 동안은 파싱 결과를 재사용한다.
|
||||
*/
|
||||
private WhiteList getWhiteList(String adptGrpName) {
|
||||
|
||||
String cacheKey = StringUtils.defaultString(adptGrpName);
|
||||
String propValue = findPropValue(adptGrpName);
|
||||
|
||||
CachedWhiteList cached = whiteListCache.get(cacheKey);
|
||||
if (cached != null && StringUtils.equals(cached.propValue, propValue)) {
|
||||
return cached.whiteList;
|
||||
}
|
||||
|
||||
WhiteList whiteList = parse(propValue);
|
||||
whiteListCache.put(cacheKey, new CachedWhiteList(propValue, whiteList));
|
||||
return whiteList;
|
||||
}
|
||||
|
||||
private String findPropValue(String adptGrpName) {
|
||||
|
||||
PropManager propManager = PropManager.getInstance();
|
||||
|
||||
if (StringUtils.isNotBlank(adptGrpName)) {
|
||||
String value = propManager.getProperty(PROPERTIES_GROUP_NAME, HEADER_KEY_NAMES + "." + adptGrpName, "");
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
return StringUtils.trimToEmpty(value);
|
||||
}
|
||||
}
|
||||
|
||||
return StringUtils.trimToEmpty(propManager.getProperty(PROPERTIES_GROUP_NAME, HEADER_KEY_NAMES, ""));
|
||||
}
|
||||
|
||||
private static WhiteList parse(String propValue) {
|
||||
|
||||
Set<String> names = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
|
||||
List<String> prefixes = new ArrayList<String>();
|
||||
|
||||
if (StringUtils.isNotBlank(propValue)) {
|
||||
for (String token : propValue.split(",")) {
|
||||
String name = StringUtils.trimToEmpty(token);
|
||||
if (name.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (name.endsWith("*")) {
|
||||
String prefix = name.substring(0, name.length() - 1);
|
||||
if (!prefix.isEmpty()) {
|
||||
prefixes.add(prefix);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
names.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
return new WhiteList(names, prefixes);
|
||||
}
|
||||
|
||||
private static Set<String> createHeaderSet(String... names) {
|
||||
Set<String> set = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
|
||||
Collections.addAll(set, names);
|
||||
return Collections.unmodifiableSet(set);
|
||||
}
|
||||
|
||||
/** 파싱된 허용 목록. 헤더명 완전일치 목록과 접두사(*) 목록으로 구성된다. */
|
||||
private static class WhiteList {
|
||||
|
||||
private final Set<String> names;
|
||||
private final List<String> prefixes;
|
||||
|
||||
WhiteList(Set<String> names, List<String> prefixes) {
|
||||
this.names = Collections.unmodifiableSet(names);
|
||||
this.prefixes = Collections.unmodifiableList(prefixes);
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return names.isEmpty() && prefixes.isEmpty();
|
||||
}
|
||||
|
||||
Set<String> getNames() {
|
||||
return names;
|
||||
}
|
||||
|
||||
boolean hasPrefix() {
|
||||
return !prefixes.isEmpty();
|
||||
}
|
||||
|
||||
boolean matchesPrefix(String headerName) {
|
||||
for (String prefix : prefixes) {
|
||||
if (StringUtils.startsWithIgnoreCase(headerName, prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static class CachedWhiteList {
|
||||
|
||||
private final String propValue;
|
||||
private final WhiteList whiteList;
|
||||
|
||||
CachedWhiteList(String propValue, WhiteList whiteList) {
|
||||
this.propValue = propValue;
|
||||
this.whiteList = whiteList;
|
||||
}
|
||||
}
|
||||
}
|
||||
-72
@@ -1,72 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter.custom;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import com.eactive.eai.common.util.JacksonUtil;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.adapter.AdapterManager;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public class JsonToSetStatusFilter implements HttpAdapterFilter {
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
JsonNode rootNode = parseJson(adptGrpName, message);
|
||||
|
||||
int httpStatus = 200;
|
||||
|
||||
if (rootNode.has("apiRsltCd") && !rootNode.get("apiRsltCd").asText().isEmpty()) {
|
||||
try {
|
||||
String statusParam = rootNode.get("apiRsltCd").asText();
|
||||
httpStatus = Integer.parseInt(statusParam);
|
||||
} catch (NumberFormatException e) {
|
||||
httpStatus = 400; // 잘못된 입력일 경우 400 Bad Request 반환
|
||||
}
|
||||
// HTTP 상태 코드 설정
|
||||
response.setStatus(httpStatus);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
JsonNode rootNode = parseJson(adptGrpName, resultMessage);
|
||||
int httpStatus = 200;
|
||||
|
||||
if (rootNode.has("apiRsltCd") && !rootNode.get("apiRsltCd").asText().isEmpty()) {
|
||||
try {
|
||||
String statusParam = rootNode.get("apiRsltCd").asText();
|
||||
httpStatus = Integer.parseInt(statusParam);
|
||||
} catch (NumberFormatException e) {
|
||||
httpStatus = 400; // 잘못된 입력일 경우 400 Bad Request 반환
|
||||
}
|
||||
// HTTP 상태 코드 설정
|
||||
response.setStatus(httpStatus);
|
||||
}
|
||||
return resultMessage;
|
||||
}
|
||||
|
||||
public JsonNode parseJson(String adptGrpName, Object message) throws UnsupportedEncodingException, JsonMappingException, JsonProcessingException {
|
||||
String charset = StringUtils.defaultIfBlank(AdapterManager.getInstance().getAdapterGroupVO(adptGrpName).getMessageEncode(), "UTF-8");
|
||||
String orgMessageString;
|
||||
if(message instanceof byte[]) {
|
||||
orgMessageString = new String((byte[])message, charset);
|
||||
} else {
|
||||
orgMessageString = (String) message;
|
||||
}
|
||||
|
||||
return mapper.readTree(JacksonUtil.escapeControlChars(orgMessageString));
|
||||
}
|
||||
}
|
||||
-113
@@ -1,113 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter.custom;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.adapter.AdapterManager;
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter;
|
||||
import com.eactive.eai.common.util.JacksonUtil;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
public class JsonToStdConverterFilter implements HttpAdapterFilter {
|
||||
// 파싱 후 rootNode.toString() 으로 재직렬화하므로 숫자 자릿수가 유실되지 않는 mapper 를 쓴다.
|
||||
// 기본 ObjectMapper 는 100000000.00 을 1.0E8 로 바꿔버린다.
|
||||
private ObjectMapper mapper = JacksonUtil.newNumberSafeMapper();
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
JsonNode rootNode = parseJson(adptGrpName, message);
|
||||
|
||||
if (rootNode.has("header_part")) {
|
||||
JsonNode headerPart = rootNode.path("header_part");
|
||||
if (!headerPart.has("eaiIntfId") || headerPart.get("eaiIntfId").asText().isEmpty()) {
|
||||
// /api/v1/public/getUserInfo.svc
|
||||
String inboundUri = prop.getProperty(HttpAdapterServiceKey.INBOUND_EXTURI);
|
||||
String apiPath = prop.getProperty(HttpAdapterServiceKey.API_PATH) + "/";
|
||||
|
||||
// getUserInfo.svc
|
||||
String eaiIntfId = org.apache.commons.lang.StringUtils.removeStart(inboundUri, apiPath);
|
||||
|
||||
// header_part 아래 eaiIntfId가 추가 또는 수정
|
||||
if (headerPart instanceof ObjectNode) {
|
||||
((ObjectNode) headerPart).put("eaiIntfId", eaiIntfId);
|
||||
// 요청 응답 구분코드 강제 설정 -> API명 찾아갈때 필요
|
||||
if (!headerPart.has("reqRspnsDscd")) {
|
||||
((ObjectNode) headerPart).put("reqRspnsDscd", "S");
|
||||
}
|
||||
|
||||
// GUID 강제 설정
|
||||
String orgnlTx = "";
|
||||
if (headerPart.has("orgnlTx") && !headerPart.get("orgnlTx").asText().isEmpty()) {
|
||||
orgnlTx = headerPart.get("orgnlTx").asText();
|
||||
}
|
||||
if (!headerPart.has("tlgrWrtnDt")) {
|
||||
((ObjectNode) headerPart).put("tlgrWrtnDt", orgnlTx);
|
||||
}
|
||||
if (!headerPart.has("tlgrCrtnSysNm")) {
|
||||
((ObjectNode) headerPart).put("tlgrCrtnSysNm", "");
|
||||
}
|
||||
if (!headerPart.has("tlgrSrlNo")) {
|
||||
((ObjectNode) headerPart).put("tlgrSrlNo", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
return message;
|
||||
}
|
||||
return rootNode.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
JsonNode rootNode = parseJson(adptGrpName, resultMessage);
|
||||
|
||||
if (rootNode.has("header_part")) {
|
||||
JsonNode headerPart = rootNode.path("header_part");
|
||||
if (!headerPart.has("eaiIntfId") || headerPart.get("eaiIntfId").asText().isEmpty()) {
|
||||
// /api/v1/public/getUserInfo.svc
|
||||
String inboundUri = prop.getProperty(HttpAdapterServiceKey.INBOUND_EXTURI);
|
||||
String apiPath = prop.getProperty(HttpAdapterServiceKey.API_PATH) + "/";
|
||||
|
||||
// getUserInfo.svc
|
||||
String eaiIntfId = org.apache.commons.lang.StringUtils.removeStart(inboundUri, apiPath);
|
||||
|
||||
// header_part 아래 eaiIntfId가 추가 또는 수정
|
||||
if (headerPart instanceof ObjectNode) {
|
||||
((ObjectNode) headerPart).put("eaiIntfId", eaiIntfId);
|
||||
// 요청 응답 구분코드 강제 설정 -> API명 찾아갈때 필요
|
||||
((ObjectNode) headerPart).put("reqRspnsDscd", "S");
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
return resultMessage;
|
||||
}
|
||||
return rootNode.toString();
|
||||
}
|
||||
|
||||
public JsonNode parseJson(String adptGrpName, Object message) throws UnsupportedEncodingException, JsonMappingException, JsonProcessingException {
|
||||
String charset = StringUtils.defaultIfBlank(AdapterManager.getInstance().getAdapterGroupVO(adptGrpName).getMessageEncode(), "UTF-8");
|
||||
String orgMessageString;
|
||||
if(message instanceof byte[]) {
|
||||
orgMessageString = new String((byte[])message, charset);
|
||||
} else {
|
||||
orgMessageString = (String) message;
|
||||
}
|
||||
|
||||
return mapper.readTree(JacksonUtil.escapeControlChars(orgMessageString));
|
||||
}
|
||||
|
||||
}
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter.custom;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Iterator;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.adapter.AdapterManager;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter;
|
||||
import com.eactive.eai.common.util.JacksonUtil;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
public class KbankEaiJsonParseFilter implements HttpAdapterFilter {
|
||||
|
||||
// 파싱 후 재직렬화하므로 숫자 자릿수가 유실되지 않는 mapper 를 쓴다.
|
||||
// 기본 ObjectMapper 는 100000000.00 을 1.0E8 로 바꿔버린다.
|
||||
private ObjectMapper mapper = JacksonUtil.newNumberSafeMapper();
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
JsonNode rootNode = parseJson(adptGrpName, message);
|
||||
|
||||
ObjectNode replacedJson = mapper.createObjectNode();
|
||||
for(Iterator<String> it = rootNode.fieldNames(); it.hasNext();) {
|
||||
String fieldName = it.next();
|
||||
String value = rootNode.get(fieldName).asText();
|
||||
JsonNode jsonNode = mapper.readTree(JacksonUtil.escapeControlChars(value));
|
||||
replacedJson.set(fieldName, jsonNode);
|
||||
}
|
||||
|
||||
return replacedJson.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
JsonNode rootNode = parseJson(adptGrpName, resultMessage);
|
||||
|
||||
ObjectNode replacedJson = mapper.createObjectNode();
|
||||
for(Iterator<String> it = rootNode.fieldNames(); it.hasNext();) {
|
||||
String fieldName = it.next();
|
||||
JsonNode childNode = rootNode.get(fieldName);
|
||||
String childString = mapper.writeValueAsString(childNode);
|
||||
replacedJson.put(fieldName, childString);
|
||||
}
|
||||
|
||||
return replacedJson.toString();
|
||||
}
|
||||
|
||||
public JsonNode parseJson(String adptGrpName, Object message) throws UnsupportedEncodingException, JsonMappingException, JsonProcessingException {
|
||||
String charset = StringUtils.defaultIfBlank(AdapterManager.getInstance().getAdapterGroupVO(adptGrpName).getMessageEncode(), "UTF-8");
|
||||
String orgMessageString;
|
||||
if(message instanceof byte[]) {
|
||||
orgMessageString = new String((byte[])message, charset);
|
||||
} else {
|
||||
orgMessageString = (String) message;
|
||||
}
|
||||
|
||||
return mapper.readTree(JacksonUtil.escapeControlChars(orgMessageString));
|
||||
}
|
||||
|
||||
}
|
||||
-216
@@ -1,216 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter.custom;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.Properties;
|
||||
import java.util.Date;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Iterator;
|
||||
import java.text.SimpleDateFormat;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.Mac;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import com.eactive.eai.adapter.AdapterManager;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.JacksonUtil;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.apache.commons.net.util.Base64;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
|
||||
import com.eactive.eai.common.server.Keys;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public class KbankHmacSha256VerifyFilter implements HttpAdapterFilter {
|
||||
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
private static final String SERVICE_CODE_UNKNOWN = "00";
|
||||
private static final int TIMESTAMP_EXPIRATION_SECONDS = 60*10*1000;
|
||||
private static final String GROUP_NAME = "HMAC_INFO";
|
||||
|
||||
private ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
|
||||
try {
|
||||
|
||||
String hmacSignature = getHeaderCaseInsensitive(request, "hmac_signature");
|
||||
String hmacTimestamp = getHeaderCaseInsensitive(request, "hmac_timestamp");
|
||||
|
||||
if(StringUtils.isBlank(hmacSignature) || StringUtils.isBlank(hmacTimestamp)) {
|
||||
throw new JwtAuthException(String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), "Can not find hmac info");
|
||||
}
|
||||
|
||||
long inputTime = new SimpleDateFormat("yyyyMMddHHmmss").parse(hmacTimestamp).getTime();
|
||||
long currentTime = new Date().getTime();
|
||||
String timeStamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
|
||||
|
||||
logger.debug("### inputTime :"+inputTime);
|
||||
logger.debug("### currentTime :"+currentTime);
|
||||
logger.debug("### timeStamp :"+timeStamp);
|
||||
|
||||
String systemMode = System.getProperty(Keys.EAI_SYSTEMMODE);
|
||||
|
||||
if(currentTime - inputTime > TIMESTAMP_EXPIRATION_SECONDS) {
|
||||
|
||||
logger.debug("### systemMode:" + systemMode);
|
||||
logger.debug("### currentTime - inputTime:" + (currentTime - inputTime));
|
||||
|
||||
if("P".equals(systemMode)) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN,"00"), "Signature verifying failed");
|
||||
}
|
||||
}
|
||||
|
||||
String sMessage = (String)message;
|
||||
|
||||
sMessage = sMessage.replace(" ","");
|
||||
sMessage = sMessage.replace("\n","");
|
||||
sMessage = sMessage.replace("\r","");
|
||||
|
||||
sMessage = sMessage + hmacTimestamp;
|
||||
|
||||
logger.info("### sMessage:"+sMessage);
|
||||
|
||||
String alCoId = "AL2021012901001";
|
||||
|
||||
JsonNode rootNode = parseJson(adptGrpName, message);
|
||||
|
||||
for(Iterator<String> it = rootNode.fieldNames(); it.hasNext();){
|
||||
String fieldName = it.next();
|
||||
if(fieldName.equals("ALCO_ID")) alCoId = rootNode.get(fieldName).asText();
|
||||
}
|
||||
|
||||
logger.info("### alCoId:"+alCoId);
|
||||
|
||||
Properties hmacProp = PropManager.getInstance().getProperties(GROUP_NAME);
|
||||
|
||||
String hmacKey = hmacProp.getProperty(alCoId+"-hmack-key","");
|
||||
String hmacKek = hmacProp.getProperty(alCoId+"-hmack-kek","");
|
||||
String hmacIvForKey = hmacProp.getProperty(alCoId+"-hmack-iv-for-key","");
|
||||
|
||||
logger.debug("### hmacKey:"+hmacKey);
|
||||
logger.debug("### hmacIvForKey:"+hmacIvForKey);
|
||||
logger.debug("### hmacKek:"+hmacKek);
|
||||
|
||||
//kbank.payment.hmac-key
|
||||
int hmacKeyLen = hmacKey.length();
|
||||
byte[] hmacKeyData = new byte[hmacKeyLen/2];
|
||||
for(int i=0;i<hmacKeyLen;i+=2){
|
||||
hmacKeyData[i/2] = (byte)((Character.digit(hmacKey.charAt(i), 16) << 4) + Character.digit(hmacKey.charAt(i+1), 16));
|
||||
}
|
||||
|
||||
//kbank.payment.hmac-kek
|
||||
int hmacKekLen = hmacKek.length();
|
||||
byte[] hmacKekData = new byte[hmacKekLen/2];
|
||||
for(int i=0;i<hmacKekLen;i+=2){
|
||||
hmacKekData[i/2] = (byte)((Character.digit(hmacKek.charAt(i),16) << 4) + Character.digit(hmacKek.charAt(i+1), 16));
|
||||
}
|
||||
|
||||
//kbank.payment.hmac-iv-for-key
|
||||
int hmacIvForKeyLen = hmacIvForKey.length();
|
||||
byte[] hmacIvForKeyData = new byte[hmacIvForKeyLen/2];
|
||||
for(int i=0;i<hmacIvForKeyLen;i+=2){
|
||||
hmacIvForKeyData[i/2] = (byte)((Character.digit(hmacIvForKey.charAt(i),16) << 4) + Character.digit(hmacIvForKey.charAt(i+1), 16));
|
||||
}
|
||||
|
||||
byte[] decrypedHmacKey = null;
|
||||
|
||||
try{
|
||||
Cipher cp = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
cp.init(Cipher.DECRYPT_MODE, new SecretKeySpec(hmacIvForKeyData,"AES"), new IvParameterSpec(hmacIvForKeyData));
|
||||
decrypedHmacKey = cp.doFinal(hmacKeyData);
|
||||
} catch (Exception e) {
|
||||
logger.debug(e.getMessage());
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), "Signature verifying failed");
|
||||
}
|
||||
|
||||
SecretKeySpec secretKey = new SecretKeySpec(decrypedHmacKey, "HmacSHA256");
|
||||
String madeSignature = "";
|
||||
|
||||
logger.info("### secretKey:"+secretKey);
|
||||
|
||||
try{
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(secretKey);
|
||||
madeSignature= Base64.encodeBase64String(mac.doFinal(sMessage.getBytes()));
|
||||
} catch (Exception e) {
|
||||
logger.debug(e.getMessage());
|
||||
throw new JwtAuthException(String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), "Signature verifying failed");
|
||||
}
|
||||
|
||||
logger.info("### madeSignature:"+madeSignature);
|
||||
|
||||
if(!StringUtils.equals(hmacSignature, madeSignature)){
|
||||
logger.info("### hmacSignature:"+hmacSignature);
|
||||
logger.info("### madeSignature:"+madeSignature);
|
||||
|
||||
throw new JwtAuthException(String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), "Signature verifying failed" );
|
||||
}
|
||||
|
||||
} catch (JwtAuthException e) {
|
||||
logger.debug(e.getMessage());
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
logger.debug(e.getMessage());
|
||||
throw new JwtAuthException(String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), "Signature verifying failed(Unknown)" );
|
||||
}
|
||||
|
||||
logger.info("### pass: "+"success");
|
||||
|
||||
return message;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 대소문자 구분 없이 HTTP 헤더 값을 가져오는 메소드
|
||||
* @param request HTTP 요청 객체
|
||||
* @param headerName 찾고자 하는 헤더 이름
|
||||
* @return 헤더 값 또는 null
|
||||
*/
|
||||
public static String getHeaderCaseInsensitive(HttpServletRequest request, String headerName) {
|
||||
Enumeration<String> headerNames = request.getHeaderNames();
|
||||
while (headerNames.hasMoreElements()) {
|
||||
String header = headerNames.nextElement();
|
||||
if (header != null && header.equalsIgnoreCase(headerName)) {
|
||||
return request.getHeader(header);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public JsonNode parseJson(String adptGrpName, Object message) throws UnsupportedEncodingException, JsonMappingException,JsonProcessingException {
|
||||
String charset = StringUtils.defaultIfBlank(AdapterManager.getInstance().getAdapterGroupVO(adptGrpName).getMessageEncode(), "UTF-8");
|
||||
String orgMessageString;
|
||||
|
||||
if(message instanceof byte[]) {
|
||||
orgMessageString = new String((byte[])message, charset);
|
||||
} else {
|
||||
orgMessageString = (String) message;
|
||||
}
|
||||
|
||||
return mapper.readTree(JacksonUtil.escapeControlChars(orgMessageString));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
return resultMessage;
|
||||
}
|
||||
|
||||
}
|
||||
-205
@@ -1,205 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter.custom;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import org.apache.commons.codec.digest.HmacAlgorithms;
|
||||
import org.apache.commons.codec.digest.HmacUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
|
||||
import com.eactive.eai.authserver.service.OAuth2Manager;
|
||||
import com.eactive.eai.authserver.vo.ClientVO;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public class SnapHmacSha512VerifyFilter implements HttpAdapterFilter {
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
private static final String SERVICE_CODE_UNKNOWN = "00";
|
||||
private static final int X_TIMESTAMP_EXPIRATION_SECONDS = 60 * 5;
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
// @formatter:off
|
||||
/*
|
||||
* HMAC_SHA512 (clientSecret, stringToSign)
|
||||
*
|
||||
* stringToSign = HTTPMethod:EndpointUrl:AccessToken
|
||||
* :Lowercase(HexEncode(SHA-256(minify(RequestBody)))):TimeStamp
|
||||
*/
|
||||
try {
|
||||
String xSignature = request.getHeader("X-SIGNATURE");
|
||||
if (StringUtils.isBlank(xSignature)) {
|
||||
throw new JwtAuthException(String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"), "Can not find X-SIGNATURE");
|
||||
}
|
||||
|
||||
String clientSecret = assignClientSecret(prop, request);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(request.getMethod())
|
||||
.append(":")
|
||||
.append(getEndpointUrl(request))
|
||||
.append(":")
|
||||
.append(SnapSimpleOauth2Filter.extractToken(request))
|
||||
.append(":")
|
||||
.append(getRequestBody((String)message))
|
||||
.append(":")
|
||||
.append(getTimeStamp(request));
|
||||
// @formatter:on
|
||||
|
||||
if (logger.isDebug()) {
|
||||
logger.debug(sb.toString());
|
||||
}
|
||||
|
||||
String hmac = new HmacUtils(HmacAlgorithms.HMAC_SHA_512, clientSecret).hmacHex(sb.toString());
|
||||
|
||||
if (!StringUtils.equals(xSignature, hmac)) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
|
||||
"Signature verifying failed");
|
||||
}
|
||||
} catch (JwtAuthException e) {
|
||||
logger.debug(e.getMessage());
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
|
||||
"Signature verifying failed(unkown)");
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private String assignClientSecret(Properties prop, HttpServletRequest request) throws Exception {
|
||||
String clientId = assignClientId(prop, request);
|
||||
if (StringUtils.isBlank(clientId)) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
|
||||
"Can not find clientId info");
|
||||
}
|
||||
|
||||
ClientVO client = (ClientVO) OAuth2Manager.getInstance().getClientDeatilsStore().get(clientId);
|
||||
if (client == null) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
|
||||
"Can not find client info");
|
||||
}
|
||||
|
||||
return client.getClientSecret();
|
||||
}
|
||||
|
||||
private String assignClientId(Properties prop, HttpServletRequest request) {
|
||||
String clientId = null;
|
||||
try {
|
||||
// 이전 filter에서 셋팅한 clientId 확보
|
||||
clientId = prop.getProperty(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID);
|
||||
|
||||
// 이전 filter에서 셋팅한 값이 없다면 header 정보에서 확보
|
||||
if (StringUtils.isBlank(clientId)) {
|
||||
clientId = request.getHeader(HttpAdapterServiceSupport.HEADER_NAME_CLIENT_ID);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
return clientId;
|
||||
}
|
||||
|
||||
private String getEndpointUrl(HttpServletRequest request) {
|
||||
String servletPath = request.getServletPath();
|
||||
String pathInfo = request.getPathInfo();
|
||||
String queryString = request.getQueryString();
|
||||
|
||||
StringBuilder url = new StringBuilder();
|
||||
url.append(servletPath);
|
||||
|
||||
if (pathInfo != null) {
|
||||
url.append(pathInfo);
|
||||
}
|
||||
|
||||
if (queryString != null) {
|
||||
url.append("?").append(queryString);
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
private String getRequestBody(String message) throws Exception {
|
||||
if (StringUtils.isEmpty(message)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Lowercase(HexEncode(SHA-256(minify(RequestBody))))
|
||||
String body = minifyJson(message);
|
||||
try {
|
||||
body = DigestUtils.sha256Hex(body);
|
||||
} catch (Exception e) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
|
||||
"SHA-256 digest error");
|
||||
}
|
||||
return body.toLowerCase();
|
||||
}
|
||||
|
||||
private String minifyJson(String json) {
|
||||
try {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
JsonNode jsonNode = objectMapper.readValue(json, JsonNode.class);
|
||||
return jsonNode.toString();
|
||||
} catch (Exception e) {
|
||||
// json이 아닐경우
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
private String getTimeStamp(HttpServletRequest request) throws Exception {
|
||||
String timeStamp = request.getHeader("X-TIMESTAMP");
|
||||
if (StringUtils.isBlank(timeStamp)) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
|
||||
"Can not find X-TIMESTAMP");
|
||||
}
|
||||
|
||||
// 2020-12-23T09:10:11+07:00(현재시간이랑 비교로직 추가-보안)
|
||||
ZonedDateTime xTimeStamp = ZonedDateTime.parse(timeStamp, DateTimeFormatter.ISO_DATE_TIME);
|
||||
xTimeStamp = xTimeStamp.plusSeconds(X_TIMESTAMP_EXPIRATION_SECONDS);
|
||||
ZonedDateTime now = ZonedDateTime.now();
|
||||
if (xTimeStamp.isBefore(now.withZoneSameInstant(xTimeStamp.getZone()))) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
|
||||
"Too old X-TIMESTAMP");
|
||||
}
|
||||
|
||||
return timeStamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
return resultMessage;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
String str = "POST:/api/test/aaa/type02/555:"
|
||||
+ "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6WyJzbmFwIl0sImV4cCI6MTY3NjUyNDI1MSwianRpIjoiNzI0MWMyZGUtZjJhNi00NTFmLWExMjQtZGM3NmI0ZGY1OThjIiwiY2xpZW50X2lkIjoic1NHSkR3bEpzZWw1V2VYb2R6ZDNKM01RMjBLWUNacEwifQ.Obe8t8IwuQ3P7i4yCSASYze-9DLmVwXe0g_gBYlwv_Jzc7V8-ooTwp6SVnaNsM6pp1GV-9lxMpAzQO_CRHQq_hdMeiPI_CXHh3gETvjQThn57QGEGdCNp_lUEVYtMPUxi5u3X6Of07o0_OB83WI7rA1zD0DaP8V67pgfq-jMRe_3IXztOYu_nwMgREbGvs9tqcsjxppRKkEHcK1S8Eq4HzaLwjwyHJAhIlTba27yd4T8ELC7IpFphJBfEPF_t0ZpYW15lOrg5pHPjy1xpTV0Kgipog5wycTZlFUeCWWjfxTrpVmt_1XlEOlZH9X6xAefWrH93_fpbt3OcxwP4u9KhA"
|
||||
+ ":424058b889b9d860d13b79d90df52ddcbefba2fc9d27607e999c7c3c4d61d9c8:" + "2023-02-16T09:47:07+07:00";
|
||||
|
||||
String hmac = new HmacUtils(HmacAlgorithms.HMAC_SHA_512,
|
||||
"o3Hre3voTrHbLueDrHC0LYM5effHQZILI8t23htbD12TKD5QJUMONqFqv4494iQS46bzHS0xW1fBC5LHo3D0SzhZvQcPUaJZw0iQ79NnzYFUCTrEsoFJRL300dp3Ql4y")
|
||||
.hmacHex(str);
|
||||
|
||||
// System.out.println(hmac);
|
||||
}
|
||||
}
|
||||
-237
@@ -1,237 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter.custom;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.text.ParseException;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthFilter;
|
||||
import com.eactive.eai.authserver.service.OAuth2Manager;
|
||||
import com.eactive.eai.common.property.PropGroupVO;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.inbound.action.ActionFactory;
|
||||
import com.eactive.eai.inbound.action.RequestAction;
|
||||
import com.eactive.eai.inbound.processor.Processor;
|
||||
import com.eactive.eai.message.StandardMessageUtil;
|
||||
import com.nimbusds.jose.JWSVerifier;
|
||||
import com.nimbusds.jose.crypto.RSASSAVerifier;
|
||||
import com.nimbusds.jose.util.IOUtils;
|
||||
import com.nimbusds.jwt.SignedJWT;
|
||||
|
||||
public class SnapJwtAuthFilter implements HttpAdapterFilter {
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String PROP_GROUP_AUTH_SERVER = "OAuthServer";
|
||||
public static final String PROP_KEYSTORE_PATH = "certification.publicKeyPath";
|
||||
|
||||
private static final String SERVICE_CODE_UNKNOWN = "00";
|
||||
|
||||
public static final String PAYLOAD_PARAM_NAME_SCOPE = "scope";
|
||||
public static final String PAYLOAD_PARAM_NAME_CLIENT_ID = "client_id";
|
||||
|
||||
private JWSVerifier jwsVerifier;
|
||||
|
||||
public SnapJwtAuthFilter() {
|
||||
super();
|
||||
|
||||
PropGroupVO vo = PropManager.getInstance().getPropGroupVO(PROP_GROUP_AUTH_SERVER);
|
||||
String publicKeyPath = "/certificate/elink-oauth-dev.pub";
|
||||
if (vo != null) {
|
||||
publicKeyPath = vo.getProperty(PROP_KEYSTORE_PATH);
|
||||
} else {
|
||||
logger.warn("The properties has not been set.[" + PROP_GROUP_AUTH_SERVER + "]");
|
||||
}
|
||||
|
||||
Resource resource = new ClassPathResource(publicKeyPath);
|
||||
String publicKey = null;
|
||||
try {
|
||||
publicKey = IOUtils.readInputStreamToString(resource.getInputStream());
|
||||
publicKey = StringUtils.replace(publicKey, "-----BEGIN PUBLIC KEY-----", "");
|
||||
publicKey = StringUtils.replace(publicKey, "-----END PUBLIC KEY-----", "");
|
||||
publicKey = StringUtils.remove(publicKey, "\r");
|
||||
publicKey = StringUtils.remove(publicKey, "\n");
|
||||
|
||||
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey));
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
jwsVerifier = new RSASSAVerifier((RSAPublicKey) keyFactory.generatePublic(keySpec));
|
||||
} catch (final IOException | NoSuchAlgorithmException | InvalidKeySpecException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
String apiId = assignApiId(adptGrpName, adptName, message, prop, request);
|
||||
if (StringUtils.isBlank(apiId)) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
|
||||
"Can not find apiId(apiCode) info");
|
||||
}
|
||||
|
||||
try {
|
||||
String token = JwtAuthFilter.extractJWTToken(request);
|
||||
SignedJWT signedJWT = SignedJWT.parse(token);
|
||||
|
||||
if (new Date().after(signedJWT.getJWTClaimsSet().getExpirationTime())) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"),
|
||||
"Invalid Token - expired (B2B)");
|
||||
}
|
||||
|
||||
if (!signedJWT.verify(jwsVerifier)) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"),
|
||||
"Invalid Token (B2B)");
|
||||
}
|
||||
|
||||
OAuth2Manager manager = OAuth2Manager.getInstance();
|
||||
HashSet<String> scopeSet = manager.getApiScopeMap().get(apiId);
|
||||
if (!verifyScope(scopeSet, signedJWT)) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"),
|
||||
String.format("Insufficient [Scope](Allowed Scope=%s)", scopeSet.toString()));
|
||||
}
|
||||
|
||||
// header 값으로 전송된 clientId와 토큰 소유 clientId check
|
||||
if (!verifyClientId(prop, signedJWT)) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
|
||||
"client_id not matched(header/token)");
|
||||
}
|
||||
} catch (JwtAuthException e) {
|
||||
logger.debug(e.getMessage());
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"),
|
||||
"Invalid Token (B2B)");
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private boolean verifyClientId(Properties prop, SignedJWT signedJWT) {
|
||||
try {
|
||||
// 이전 filter에서 셋팅한 clientId 확보
|
||||
String headerClientId = prop.getProperty(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID);
|
||||
String tokenClientId = (String) signedJWT.getJWTClaimsSet().getClaim(PAYLOAD_PARAM_NAME_CLIENT_ID);
|
||||
|
||||
// 이전 filter에서 셋팅한 값이 없다면 token 정보에서 확보
|
||||
if (StringUtils.isBlank(headerClientId)) {
|
||||
if (StringUtils.isNotBlank(tokenClientId)) {
|
||||
prop.put(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID, tokenClientId);
|
||||
}
|
||||
} else { // header로 전달된 clientId가 있을때만 비교
|
||||
if (!headerClientId.equals(tokenClientId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private String assignApiId(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request) {
|
||||
String apiId = null;
|
||||
try {
|
||||
String actionName = prop.getProperty(Processor.REQUEST_ACTION);
|
||||
RequestAction action = ActionFactory.createAction(actionName);
|
||||
action.setAdapterInfo(adptGrpName, adptName, prop);
|
||||
String[] keys = action.perform(message);
|
||||
apiId = keys[0];
|
||||
|
||||
// PathVariable 지원 추가
|
||||
apiId = StandardMessageUtil.getMatchedKey(apiId, actionName);
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
// // header 에서 확보
|
||||
// String apiId = request.getHeader(HEADER_NAME_API_CODE);
|
||||
// if (StringUtils.isBlank(apiId)) {
|
||||
// // url에서 확보
|
||||
// // /ONLWeb/api/v1/public/getUserInfo.svc/
|
||||
// apiId = request.getRequestURI();
|
||||
// // /ONLWeb/api/v1/public/getUserInfo.svc
|
||||
// apiId = StringUtils.removeEnd(apiId, "/");
|
||||
// // getUserInfo.svc
|
||||
// apiId = StringUtils.substringAfterLast(apiId, "/");
|
||||
// }
|
||||
|
||||
return apiId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
return resultMessage;
|
||||
}
|
||||
|
||||
private boolean verifyScope(HashSet<String> scopeSet, SignedJWT signedJWT) throws ParseException {
|
||||
if (scopeSet == null || scopeSet.isEmpty()) {
|
||||
return true; // If no scope is specified in the api, all are allowed
|
||||
}
|
||||
|
||||
String[] scopeArr = signedJWT.getJWTClaimsSet().getStringArrayClaim(PAYLOAD_PARAM_NAME_SCOPE);
|
||||
if (scopeArr == null || scopeArr.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (String scope : scopeArr) {
|
||||
if (scopeSet.contains(scope)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static String extractJWTToken(HttpServletRequest request) throws JwtAuthException {
|
||||
String authorization = request.getHeader("Authorization");
|
||||
if (StringUtils.isBlank(authorization)) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
|
||||
"No have header info: Authorization");
|
||||
}
|
||||
|
||||
String[] components = authorization.split("\\s");
|
||||
|
||||
if (components.length != 2) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
|
||||
"Malformat [Authorization] content");
|
||||
}
|
||||
|
||||
if (!StringUtils.equalsIgnoreCase(components[0], "Bearer")) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
|
||||
"[Bearer] is needed");
|
||||
}
|
||||
|
||||
return components[1].trim();
|
||||
}
|
||||
}
|
||||
-205
@@ -1,205 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.filter.custom;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.oauth2.common.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.provider.OAuth2Authentication;
|
||||
import org.springframework.security.oauth2.provider.token.TokenStore;
|
||||
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.HttpAdapterFilter;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
|
||||
import com.eactive.eai.authserver.service.OAuth2Manager;
|
||||
import com.eactive.eai.authserver.util.BeanUtils;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.inbound.action.ActionFactory;
|
||||
import com.eactive.eai.inbound.action.RequestAction;
|
||||
import com.eactive.eai.inbound.processor.Processor;
|
||||
import com.eactive.eai.message.StandardMessageUtil;
|
||||
|
||||
public class SnapSimpleOauth2Filter implements HttpAdapterFilter {
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
private static final String SERVICE_CODE_UNKNOWN = "00";
|
||||
|
||||
private TokenStore tokenStore;
|
||||
|
||||
public SnapSimpleOauth2Filter() {
|
||||
super();
|
||||
tokenStore = BeanUtils.getBean("tokenStore", TokenStore.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPreFilter(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
try {
|
||||
String apiId = assignApiId(adptGrpName, adptName, message, prop, request);
|
||||
if (StringUtils.isBlank(apiId)) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
|
||||
"Can not find apiId(apiCode) info");
|
||||
}
|
||||
|
||||
String tokenStr = extractToken(request);
|
||||
|
||||
OAuth2AccessToken token = tokenStore.readAccessToken(tokenStr);
|
||||
if (token == null) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "03"),
|
||||
"Token Not Found (B2B)");
|
||||
}
|
||||
|
||||
if (token.isExpired()) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"),
|
||||
"Invalid Token - expired (B2B)");
|
||||
}
|
||||
|
||||
OAuth2Authentication authentication = tokenStore.readAuthentication(token);
|
||||
if (!authentication.isAuthenticated()) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"),
|
||||
"Invalid Token (B2B)");
|
||||
}
|
||||
|
||||
// check scope
|
||||
OAuth2Manager manager = OAuth2Manager.getInstance();
|
||||
HashSet<String> scopeSet = manager.getApiScopeMap().get(apiId);
|
||||
if (!verifyScope(scopeSet, authentication)) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"),
|
||||
String.format("Insufficient [Scope](Allowed Scope=%s)", scopeSet.toString()));
|
||||
}
|
||||
|
||||
// header 값으로 전송된 clientId와 토큰 소유 clientId check
|
||||
if (!verifyClientId(prop, authentication)) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
|
||||
"client_id not matched(header/token)");
|
||||
}
|
||||
} catch (JwtAuthException e) {
|
||||
logger.debug(e.getMessage());
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "01"),
|
||||
"Invalid Token (B2B)");
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
private boolean verifyScope(HashSet<String> scopeSet, OAuth2Authentication auth) {
|
||||
if (scopeSet == null || scopeSet.isEmpty()) {
|
||||
return true; // If no scope is specified in the api, all are allowed
|
||||
}
|
||||
|
||||
Set<String> scopesInToken = auth.getOAuth2Request().getScope();
|
||||
|
||||
if (scopesInToken == null || scopesInToken.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (String scope : scopesInToken) {
|
||||
if (scopeSet.contains(scope)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean verifyClientId(Properties prop, Authentication authentication) {
|
||||
try {
|
||||
// 이전 filter에서 셋팅한 clientId 확보
|
||||
String headerClientId = prop.getProperty(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID);
|
||||
String tokenClientId = ((OAuth2Authentication) authentication).getOAuth2Request().getClientId();
|
||||
|
||||
// 이전 filter에서 셋팅한 값이 없다면 token 정보에서 확보
|
||||
if (StringUtils.isBlank(headerClientId)) {
|
||||
if (StringUtils.isNotBlank(tokenClientId)) {
|
||||
prop.put(HttpAdapterServiceSupport.PROPERTIES_NAME_CLIENT_ID, tokenClientId);
|
||||
}
|
||||
} else { // header로 전달된 clientId가 있을때만 비교
|
||||
if (!headerClientId.equals(tokenClientId)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private String assignApiId(String adptGrpName, String adptName, Object message, Properties prop,
|
||||
HttpServletRequest request) {
|
||||
String apiId = null;
|
||||
try {
|
||||
String actionName = prop.getProperty(Processor.REQUEST_ACTION);
|
||||
RequestAction action = ActionFactory.createAction(actionName);
|
||||
action.setAdapterInfo(adptGrpName, adptName, prop);
|
||||
String[] keys = action.perform(message);
|
||||
apiId = keys[0];
|
||||
|
||||
// PathVariable 지원 추가
|
||||
apiId = StandardMessageUtil.getMatchedKey(apiId, actionName);
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
|
||||
// // header 에서 확보
|
||||
// String apiId = request.getHeader(HEADER_NAME_API_CODE);
|
||||
// if (StringUtils.isBlank(apiId)) {
|
||||
// // url에서 확보
|
||||
// // /ONLWeb/api/v1/public/getUserInfo.svc/
|
||||
// apiId = request.getRequestURI();
|
||||
// // /ONLWeb/api/v1/public/getUserInfo.svc
|
||||
// apiId = StringUtils.removeEnd(apiId, "/");
|
||||
// // getUserInfo.svc
|
||||
// apiId = StringUtils.substringAfterLast(apiId, "/");
|
||||
// }
|
||||
|
||||
return apiId;
|
||||
}
|
||||
|
||||
public static String extractToken(HttpServletRequest request) throws JwtAuthException {
|
||||
String authorization = request.getHeader("Authorization");
|
||||
if (StringUtils.isBlank(authorization)) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
|
||||
"No have header info: Authorization");
|
||||
}
|
||||
|
||||
String[] components = authorization.split("\\s");
|
||||
|
||||
if (components.length != 2) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
|
||||
"Malformat [Authorization] content");
|
||||
}
|
||||
|
||||
if (!StringUtils.equalsIgnoreCase(components[0], "Bearer")) {
|
||||
throw new JwtAuthException(
|
||||
String.format("%d%s%s", HttpStatus.UNAUTHORIZED.value(), SERVICE_CODE_UNKNOWN, "00"),
|
||||
"[Bearer] is needed");
|
||||
}
|
||||
|
||||
return components[1].trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object doPostFilter(String adptGrpName, String adptName, Object resultMessage, Properties prop,
|
||||
HttpServletRequest request, HttpServletResponse response) throws Exception {
|
||||
return resultMessage;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -45,7 +45,7 @@ public class HttpAdapterServiceBypass extends HttpAdapterServiceSupport {
|
||||
public static final String HTTP_STATUS = "HTTP_STATUS";
|
||||
public static final String PROPERTIES_NAME_HTTP_REQUEST_METHOD = "httpRequestMethod";
|
||||
public static final String[] HOP_BY_HOP_HEADERS = new String[] { "Connection", "Keep-Alive", "Proxy-Authenticate",
|
||||
"Proxy-Authorization", "TE", "Trailers", "Trailer", "Transfer-Encoding", "Upgrade" };
|
||||
"Proxy-Authorization", "TE", "Trailers", "Transfer-Encoding", "Upgrade" };
|
||||
|
||||
protected boolean doPreserveCookies = false;
|
||||
protected boolean doPreserveCookiePath = false;
|
||||
|
||||
+67
-98
@@ -1,27 +1,5 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.impl;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.time.StopWatch;
|
||||
import org.apache.mina.common.ByteBuffer;
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
|
||||
import com.eactive.eai.adapter.AdapterGroupVO;
|
||||
import com.eactive.eai.adapter.AdapterManager;
|
||||
import com.eactive.eai.adapter.AdapterPropManager;
|
||||
@@ -30,8 +8,8 @@ import com.eactive.eai.adapter.Keys;
|
||||
import com.eactive.eai.adapter.http.HttpMemoryLogger;
|
||||
import com.eactive.eai.adapter.http.HttpStatusException;
|
||||
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey;
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport;
|
||||
import com.eactive.eai.adapter.http.dynamic.UnknownMessageLogUtils;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.exception.ExceptionUtil;
|
||||
import com.eactive.eai.common.util.CommonLib;
|
||||
@@ -41,6 +19,27 @@ import com.eactive.eai.common.util.MessageUtil;
|
||||
import com.eactive.eai.env.ElinkConfig;
|
||||
import com.eactive.eai.inbound.processor.Processor;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.time.StopWatch;
|
||||
import org.apache.mina.common.ByteBuffer;
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
|
||||
public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport
|
||||
{
|
||||
public static final String PROPERTIES_NAME_HTTP_REQUEST_METHOD = "httpRequestMethod";
|
||||
@@ -64,8 +63,7 @@ public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport
|
||||
|
||||
AdapterGroupVO adapterGroupVo = null;
|
||||
Properties prop = null;
|
||||
byte[] requestBytes = null;
|
||||
String encode = "MS949";
|
||||
|
||||
try {
|
||||
AdapterManager adapterManager = AdapterManager.getInstance();
|
||||
AdapterVO adptVO = adapterManager.getAdapterVO(adptGrpName,adptName);
|
||||
@@ -77,9 +75,9 @@ public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport
|
||||
}
|
||||
|
||||
Properties httpProp = manager.getProperties(adptVO.getPropGroupName());
|
||||
encode = StringUtils.defaultIfBlank(adapterManager.getAdapterGroupVO(adptGrpName).getMessageEncode(), "MS949");
|
||||
String responseType = httpProp.getProperty(RESPONSE_TYPE,"SYNC");
|
||||
String urlDecodeYn = httpProp.getProperty(URL_DECODE_YN,"N");
|
||||
String encode = StringUtils.defaultIfBlank(adapterManager.getAdapterGroupVO(adptGrpName).getMessageEncode(), "MS949");
|
||||
String messageType = adapterManager.getAdapterGroupVO(adptGrpName).getMessageType();
|
||||
String requestBodyYn = httpProp.getProperty(REQUEST_BODY_YN,"N");
|
||||
String parameterName = httpProp.getProperty(PARAMETER_NAME,PARAMETER_MESSAGE);
|
||||
@@ -106,6 +104,7 @@ public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport
|
||||
|
||||
logger.info("시작 >> encode = [" + encode + "]");
|
||||
|
||||
byte[] requestBytes = null;
|
||||
|
||||
if ("Y".equals(requestBodyYn)){
|
||||
ServletInputStream sis = request.getInputStream();
|
||||
@@ -189,19 +188,19 @@ public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport
|
||||
prop.put(PROPERTIES_NAME_HTTP_REQUEST_METHOD, request.getMethod());
|
||||
prop.put(ALLOW_IP, httpProp.getProperty(ALLOW_IP, ""));
|
||||
|
||||
// String body = new String ( requestBytes,encode);
|
||||
//
|
||||
// try {
|
||||
// Object root = JSONValue.parse(body);
|
||||
// if( root instanceof JSONArray ) {
|
||||
// JSONObject wrappedObject = new JSONObject();
|
||||
// wrappedObject.put("DJB_ROOTLESS_ARRAY", root);
|
||||
// requestBytes = wrappedObject.toJSONString().getBytes(encode);
|
||||
// }
|
||||
//
|
||||
// }catch (Exception e) {
|
||||
// // ignore json이 아니기에 해줄것이 없음.
|
||||
// }
|
||||
String body = new String ( requestBytes,encode);
|
||||
|
||||
try {
|
||||
Object root = JSONValue.parse(body);
|
||||
if( root instanceof JSONArray ) {
|
||||
JSONObject wrappedObject = new JSONObject();
|
||||
wrappedObject.put("KJB_ROOTLESS_ARRAY", root);
|
||||
requestBytes = wrappedObject.toJSONString().getBytes(encode);
|
||||
}
|
||||
|
||||
}catch (Exception e) {
|
||||
// ignore json이 아니기에 해줄것이 없음.
|
||||
}
|
||||
|
||||
// 로컬 서비스 호출
|
||||
Object result = null;
|
||||
@@ -213,11 +212,6 @@ public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport
|
||||
|
||||
stopWatch.stop();
|
||||
|
||||
String apiInternalErrorCode = prop.getProperty("API_INTERNAL_ERROR_CODE", "");
|
||||
if(StringUtils.isNotEmpty(apiInternalErrorCode))
|
||||
response.setStatus(500);
|
||||
|
||||
byte[] responseBytes = null;
|
||||
String responseData = "";
|
||||
if (RESPONSE_TYPE_ASYNC.equals(responseType)){
|
||||
if (stopWatch.getTime() > slowTranTime){
|
||||
@@ -228,22 +222,17 @@ public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport
|
||||
|
||||
if ( result == null) {
|
||||
responseData = ElinkConfig.getAsyncDummyDataForAdapterGroup(adptGrpName);
|
||||
responseBytes = responseData.getBytes();
|
||||
} else if (result instanceof byte[]) {
|
||||
responseData = new String((byte[]) result, encode);
|
||||
responseBytes = (byte[]) result;
|
||||
} else if (result instanceof String) {
|
||||
responseBytes = ((String)result).getBytes();
|
||||
if ( StringUtils.isBlank((String)result ) ) {
|
||||
responseData = (String) result;
|
||||
if ( StringUtils.isBlank( responseData ) ) {
|
||||
responseData = ElinkConfig.getAsyncDummyDataForAdapterGroup(adptGrpName, adptName);
|
||||
responseBytes = responseData.getBytes();
|
||||
}
|
||||
}
|
||||
|
||||
response.setCharacterEncoding(encode);
|
||||
// response.getWriter().print(responseData);
|
||||
response.getOutputStream().write(responseBytes);
|
||||
|
||||
response.getWriter().print(responseData);
|
||||
if (traceLevel >= 3){
|
||||
HttpMemoryLogger.txlog(adptGrpName+adptName, "SEND "+"["+responseData+"]"+
|
||||
((responseData == null)?"":CommonLib.getDumpMessage(responseData))
|
||||
@@ -251,58 +240,45 @@ public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport
|
||||
}
|
||||
} else {
|
||||
if(requestBytes != null) {
|
||||
if ( result == null) {
|
||||
responseData = ElinkConfig.getAsyncDummyDataForAdapterGroup(adptGrpName);
|
||||
responseBytes = responseData.getBytes();
|
||||
} else if (result instanceof byte[]) {
|
||||
if (result instanceof byte[]) {
|
||||
responseData = new String((byte[]) result, encode);
|
||||
responseBytes = (byte[]) result;
|
||||
} else if (result instanceof String) {
|
||||
responseBytes = ((String)result).getBytes(encode);
|
||||
if ( StringUtils.isBlank((String)result ) ) {
|
||||
responseData = ElinkConfig.getAsyncDummyDataForAdapterGroup(adptGrpName, adptName);
|
||||
responseBytes = responseData.getBytes(encode);
|
||||
}
|
||||
responseData = (String) result;
|
||||
}
|
||||
|
||||
|
||||
logger.info("종료 >> encode = [" + encode + "]");
|
||||
|
||||
if (addData.length() > 0){
|
||||
responseData = addData + responseData;
|
||||
responseBytes = responseData.getBytes();
|
||||
}
|
||||
|
||||
// try {
|
||||
// Object root = JSONValue.parse(responseData);
|
||||
// if( root instanceof JSONObject ) {
|
||||
// JSONObject wrappedObject = (JSONObject) root;
|
||||
// responseData = wrappedObject.get("DJB_ROOTLESS_ARRAY").toString();
|
||||
// }
|
||||
//
|
||||
// }catch (Exception e) {
|
||||
// // ignore json이 아니기에 해줄것이 없음.
|
||||
// }
|
||||
try {
|
||||
Object root = JSONValue.parse(responseData);
|
||||
if( root instanceof JSONObject ) {
|
||||
JSONObject wrappedObject = (JSONObject) root;
|
||||
responseData = wrappedObject.get("KJB_ROOTLESS_ARRAY").toString();
|
||||
}
|
||||
|
||||
}catch (Exception e) {
|
||||
// ignore json이 아니기에 해줄것이 없음.
|
||||
}
|
||||
|
||||
// 300응답에서 받은 Header정보 사용하기 위해 읽어옴.
|
||||
// Map<String, Object> responsePropertyMap = (Map<String, Object>) prop.get(OUTBOUND_PROPERTY_MAP);
|
||||
// if(responsePropertyMap != null){
|
||||
// Properties responseHeaders = (Properties)responsePropertyMap.get(OUTBOUND_RESPONSE_HEADERS);
|
||||
// Map<String, String> responseHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
// // Properties 내용을 모두 복사
|
||||
// for (String name : responseHeaders.stringPropertyNames()) {
|
||||
// responseHeaderMap.put(name, responseHeaders.getProperty(name));
|
||||
// }
|
||||
// String contentTytpe = responseHeaderMap.get("Content-Type");
|
||||
// response.setContentType(contentTytpe); // 300응답에서 받은 Content-Type을 그대로 적용.
|
||||
// }
|
||||
Map<String, Object> responsePropertyMap = (Map<String, Object>) prop.get(OUTBOUND_PROPERTY_MAP);
|
||||
if(responsePropertyMap != null){
|
||||
Properties responseHeaders = (Properties)responsePropertyMap.get(OUTBOUND_RESPONSE_HEADERS);
|
||||
Map<String, String> responseHeaderMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
// Properties 내용을 모두 복사
|
||||
for (String name : responseHeaders.stringPropertyNames()) {
|
||||
responseHeaderMap.put(name, responseHeaders.getProperty(name));
|
||||
}
|
||||
String contentTytpe = responseHeaderMap.get("Content-Type");
|
||||
response.setContentType(contentTytpe); // 300응답에서 받은 Content-Type을 그대로 적용.
|
||||
}
|
||||
|
||||
// UI와 통신시(UTF-8) 변환오류로 ENCODE 제거
|
||||
response.setCharacterEncoding(encode);
|
||||
// response.getWriter().print(responseData);
|
||||
String resContentType = request.getContentType();
|
||||
response.setContentType(resContentType);
|
||||
response.getOutputStream().write(responseBytes);
|
||||
|
||||
response.getWriter().print(responseData);
|
||||
if (logger.isDebug()){
|
||||
logger.debug("HttpAdapter] SEND ("+adptGrpName+") = [" + responseData + "]");
|
||||
logger.debug("HttpAdapter] SEND ("+adptGrpName+") = " +
|
||||
@@ -340,14 +316,7 @@ public class HttpAdapterServiceStandard extends HttpAdapterServiceSupport
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
UnknownMessageLogUtils.logUnkownMessage(adptGrpName, adptName, requestBytes != null ? new String(requestBytes, encode) : null, prop, request, response,
|
||||
"RECEAIIRP202", e);
|
||||
} catch (UnsupportedEncodingException e1) {
|
||||
UnknownMessageLogUtils.logUnkownMessage(adptGrpName, adptName, requestBytes != null ? new String(requestBytes) : null, prop, request, response,
|
||||
"RECEAIIRP202", e);
|
||||
}
|
||||
if (traceLevel >= 3){
|
||||
if (traceLevel >= 3){
|
||||
HttpMemoryLogger.error(adptGrpName+adptName, e.toString(),e);
|
||||
}
|
||||
logger.error("HttpAdapter] "+adptGrpName+"-"+adptName, e);
|
||||
|
||||
-766
@@ -1,766 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.dynamic.impl;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.commons.fileupload.FileItem;
|
||||
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
|
||||
import org.apache.commons.fileupload.servlet.ServletFileUpload;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.time.StopWatch;
|
||||
import org.apache.mina.common.ByteBuffer;
|
||||
import org.json.simple.JSONArray;
|
||||
import org.json.simple.JSONObject;
|
||||
import org.json.simple.JSONValue;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
|
||||
import com.eactive.eai.adapter.AdapterManager;
|
||||
import com.eactive.eai.adapter.AdapterPropManager;
|
||||
import com.eactive.eai.adapter.AdapterVO;
|
||||
import com.eactive.eai.adapter.Keys;
|
||||
import com.eactive.eai.adapter.http.HttpMemoryLogger;
|
||||
import com.eactive.eai.adapter.http.HttpMethodType;
|
||||
import com.eactive.eai.adapter.http.HttpStatusException;
|
||||
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey;
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceSupport;
|
||||
import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.exception.ExceptionUtil;
|
||||
import com.eactive.eai.common.message.MessageType;
|
||||
import com.eactive.eai.common.util.CommonLib;
|
||||
import com.eactive.eai.common.util.HttpAdapterExtraLogUtil;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.common.util.MessageUtil;
|
||||
import com.eactive.eai.env.ElinkConfig;
|
||||
import com.eactive.eai.inbound.action.ActionFactory;
|
||||
import com.eactive.eai.inbound.action.RequestAction;
|
||||
import com.eactive.eai.inbound.processor.Processor;
|
||||
import com.eactive.eai.message.StandardMessageUtil;
|
||||
|
||||
/*
|
||||
* Kbank 가상계좌 INBOUND
|
||||
* @see com.eactive.eai.custom.adapter.http.dynamic.filter.VirtualAccountCryptoFilter
|
||||
* @Deprecated
|
||||
*/
|
||||
// FIXME : kbank - kbank에서는 Controller 방식을 사용하므로, 이 어댑터는 사용되지 않음 (VirtualAccountCryptoFilter 로 대체)
|
||||
public class HttpAdapterServiceVirtualAccount extends HttpAdapterServiceSupport {
|
||||
public static final String HEADER_GROUP = "HEADER_GROUP";
|
||||
// HEADER_GROUP JSON에 추가할 항목 정의, 없으면 전체 header 추가
|
||||
public static final String HEADER_KEYS = "HEADER_KEYS";
|
||||
public static final String HTTP_STATUS = "HTTP_STATUS";
|
||||
public static final String PROPERTIES_NAME_HTTP_REQUEST_METHOD = "httpRequestMethod";
|
||||
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
private String logPrefix = "HttpAdapterServiceRest] ";
|
||||
|
||||
private static final String JSON_CONTENT_TYPE = "application/json";
|
||||
private static final String JSON_FIELD_NAME = "json-body";
|
||||
private static final String FILE_GROUP_NAME = "image-file";
|
||||
private static final String UPLOAD_ROOT_PATH = "UPLOAD_ROOT_PATH";
|
||||
|
||||
private Properties addCryptoFilter(Properties prop) {
|
||||
String cryptoFilterName = "com.eactive.eai.custom.adapter.http.dynamic.filter.VirtualAccountCryptoFilter";
|
||||
String addedPreFilter = prop.getProperty(PRE_FILTERS);
|
||||
String addedPostFilter = prop.getProperty(POST_FILTERS);
|
||||
|
||||
if(StringUtils.isBlank(addedPreFilter)) {
|
||||
addedPreFilter = cryptoFilterName;
|
||||
}
|
||||
else {
|
||||
addedPreFilter = addedPreFilter + "," +cryptoFilterName;
|
||||
}
|
||||
|
||||
if(StringUtils.isBlank(addedPostFilter)) {
|
||||
addedPostFilter = cryptoFilterName;
|
||||
}
|
||||
else {
|
||||
addedPostFilter = cryptoFilterName + "," +addedPostFilter;
|
||||
}
|
||||
|
||||
prop.setProperty(PRE_FILTERS, addedPreFilter);
|
||||
prop.setProperty(POST_FILTERS, addedPostFilter);
|
||||
return prop;
|
||||
}
|
||||
|
||||
private String readMultipartBody(HttpServletRequest request) throws Exception {
|
||||
String jsonString = null;
|
||||
// Create a factory for disk-based file items
|
||||
DiskFileItemFactory factory = new DiskFileItemFactory();
|
||||
|
||||
// Set the maximum size of the files to be uploaded
|
||||
factory.setSizeThreshold(1024 * 1024);
|
||||
|
||||
// Set the temporary directory to store uploaded files
|
||||
File tempDir = (File) request.getSession().getServletContext().getAttribute("javax.servlet.context.tempdir");
|
||||
factory.setRepository(tempDir);
|
||||
|
||||
ServletFileUpload upload = new ServletFileUpload(factory);
|
||||
Map<String, String> fileMap = new HashMap<>();
|
||||
|
||||
InputStream fin = null;
|
||||
try {
|
||||
byte[] buffer = new byte[1024];
|
||||
int read = 0;
|
||||
|
||||
List<FileItem> items = upload.parseRequest(request);
|
||||
for (FileItem item : items) {
|
||||
if (!item.isFormField()) {
|
||||
// file
|
||||
String fieldName = item.getFieldName();
|
||||
String fileName = item.getName();
|
||||
fin = item.getInputStream();
|
||||
ByteArrayOutputStream fo = new ByteArrayOutputStream();
|
||||
while ((read = fin.read(buffer)) > 0) {
|
||||
fo.write(buffer, 0, read);
|
||||
}
|
||||
int fileSize = fo.size();
|
||||
byte[] fileBytes = fo.toByteArray();
|
||||
String fileContents = new String(fileBytes);
|
||||
if (logger.isInfo()) {
|
||||
logger.info("[FILE]-------------------------------------------------->");
|
||||
logger.info("Field name = " + fieldName);
|
||||
logger.info("File name = " + fileName + " contents length = " + fileSize);
|
||||
logger.info("File Contents [" + fileContents + "]");
|
||||
logger.info("[FILE]<--------------------------------------------------");
|
||||
}
|
||||
fileMap.put(fileName, fileContents);
|
||||
fin.close();
|
||||
} else {
|
||||
// regular form field
|
||||
String fieldName = item.getFieldName();
|
||||
String fieldValue = item.getString();
|
||||
if (logger.isInfo()) {
|
||||
logger.info("[FIELD] " + fieldName + " [" + fieldValue + "]");
|
||||
}
|
||||
if (JSON_CONTENT_TYPE.equalsIgnoreCase(item.getContentType())
|
||||
|| JSON_FIELD_NAME.equals(fieldName)) {
|
||||
jsonString = fieldValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isInfo()) {
|
||||
logger.info("Json body [" + jsonString + "]");
|
||||
}
|
||||
|
||||
if (jsonString == null) {
|
||||
jsonString = "{}";
|
||||
} else {
|
||||
// parsing json & add file contents
|
||||
JSONObject jsonObject = (JSONObject) JSONValue.parse(jsonString);
|
||||
if (jsonObject == null) {
|
||||
jsonString = "{}";
|
||||
} else {
|
||||
JSONObject fileGroup = new JSONObject();
|
||||
|
||||
for (Map.Entry<String, String> entry : fileMap.entrySet()) {
|
||||
fileGroup.put("fileName", entry.getKey());
|
||||
fileGroup.put("fileContents", entry.getValue());
|
||||
}
|
||||
jsonObject.put(FILE_GROUP_NAME, fileGroup);
|
||||
jsonString = jsonObject.toJSONString();
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isInfo()) {
|
||||
logger.info("Json with file [" + jsonString + "]");
|
||||
}
|
||||
return jsonString;
|
||||
} catch (Exception e) {
|
||||
logger.error("Read multipart body error.", e);
|
||||
throw e;
|
||||
} finally {
|
||||
if (fin != null) {
|
||||
try {
|
||||
fin.close();
|
||||
} catch (Exception ex) {
|
||||
// empty
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String checkRootPath(String path) {
|
||||
if (StringUtils.isEmpty(path)) {
|
||||
logger.info("upload dir not set(UPLOAD_ROOT_PATH), use system temp " + path);
|
||||
return System.getProperty("java.io.tmpdir");
|
||||
}
|
||||
File file = new File(path);
|
||||
if (!file.exists()) {
|
||||
file.mkdirs();
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private String uploadMultipartBody(HttpServletRequest request, String uploadRootPath) throws Exception {
|
||||
String jsonString = null;
|
||||
// Create a factory for disk-based file items
|
||||
DiskFileItemFactory factory = new DiskFileItemFactory();
|
||||
|
||||
// Set the maximum size of the files to be uploaded
|
||||
factory.setSizeThreshold(1024 * 1024);
|
||||
|
||||
// Set the temporary directory to store uploaded files
|
||||
File tempDir = (File) request.getSession().getServletContext().getAttribute("javax.servlet.context.tempdir");
|
||||
factory.setRepository(tempDir);
|
||||
|
||||
ServletFileUpload upload = new ServletFileUpload(factory);
|
||||
try {
|
||||
// if not exist, create folders
|
||||
String uploadDir = checkRootPath(uploadRootPath);
|
||||
List<FileItem> items = upload.parseRequest(request);
|
||||
for (FileItem item : items) {
|
||||
if (!item.isFormField()) {
|
||||
// file
|
||||
String fieldName = item.getFieldName();
|
||||
String fileName = item.getName();
|
||||
String uploadFilePath = uploadDir + File.separator + fileName;
|
||||
File uploadFile = new File(uploadFilePath);
|
||||
item.write(uploadFile);
|
||||
|
||||
if (logger.isInfo()) {
|
||||
logger.info("[FILE]-------------------------------------------------->");
|
||||
logger.info("Field name = " + fieldName);
|
||||
logger.info("File name = " + fileName + " path = " + uploadFile.getAbsolutePath());
|
||||
logger.info("[FILE]<--------------------------------------------------");
|
||||
}
|
||||
} else {
|
||||
// regular form field
|
||||
String fieldName = item.getFieldName();
|
||||
String fieldValue = item.getString();
|
||||
if (logger.isInfo()) {
|
||||
logger.info("[FIELD] " + fieldName + " [" + fieldValue + "]");
|
||||
}
|
||||
if (JSON_CONTENT_TYPE.equalsIgnoreCase(item.getContentType())
|
||||
|| JSON_FIELD_NAME.equals(fieldName)) {
|
||||
jsonString = fieldValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (logger.isInfo()) {
|
||||
logger.info("Json body [" + jsonString + "]");
|
||||
}
|
||||
return jsonString;
|
||||
} catch (Exception e) {
|
||||
logger.error("Read multipart body error.", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "deprecation" })
|
||||
public void service(String adptGrpName, String adptName, HttpServletRequest request, HttpServletResponse response) {
|
||||
int traceLevel = 0;
|
||||
|
||||
AdapterVO adptVO = null;
|
||||
AdapterPropManager manager = null;
|
||||
|
||||
Properties httpProp = null;
|
||||
String responseType = null;
|
||||
String urlDecodeYn = null;
|
||||
String encode = null;
|
||||
|
||||
String traceLevelTemp = null;
|
||||
String relayRequestHeaderKeys = null;
|
||||
String headerGroupName = null;
|
||||
|
||||
boolean isParameterType = false;
|
||||
String message = null;
|
||||
|
||||
StopWatch stopWatch = null;
|
||||
Properties prop = null;
|
||||
String paramValue = null;
|
||||
String adptMsgType = null;
|
||||
String errorResponseFormat = null;
|
||||
String uploadRootPath = null;
|
||||
try {
|
||||
AdapterManager adapterManager = AdapterManager.getInstance();
|
||||
adptVO = adapterManager.getAdapterVO(adptGrpName, adptName);
|
||||
if (adptVO == null) {
|
||||
throw new Exception("Adapter not found error");
|
||||
}
|
||||
|
||||
manager = AdapterPropManager.getInstance();
|
||||
|
||||
httpProp = manager.getProperties(adptVO.getPropGroupName());
|
||||
responseType = httpProp.getProperty(RESPONSE_TYPE, "SYNC");
|
||||
urlDecodeYn = httpProp.getProperty(URL_DECODE_YN, "N");
|
||||
// encode = httpProp.getProperty(ENCODE, "UTF-8");
|
||||
encode = StringUtils.defaultIfBlank(adapterManager.getAdapterGroupVO(adptGrpName).getMessageEncode(),
|
||||
"UTF-8");
|
||||
traceLevelTemp = httpProp.getProperty(TRACE_LEVEL, "0");
|
||||
relayRequestHeaderKeys = httpProp.getProperty(HEADER_KEYS);
|
||||
headerGroupName = httpProp.getProperty(HEADER_GROUP);
|
||||
errorResponseFormat = httpProp.getProperty(ERROR_RESPONSE_FORMAT);
|
||||
uploadRootPath = httpProp.getProperty(UPLOAD_ROOT_PATH);
|
||||
prop = new Properties();
|
||||
prop.put(INBOUND_METHOD, request.getMethod());
|
||||
prop.put(INBOUND_URI, request.getRequestURI());
|
||||
prop.put(INBOUND_HEADER, getHeaders(request));
|
||||
prop.put(INBOUND_EXTPARAMS, StringUtils.defaultString(request.getQueryString()));
|
||||
if (StringUtils.equals(adptVO.getAdapterGroupVO().getType(), Keys.TYPE_REST)
|
||||
|| StringUtils.equals(adptVO.getAdapterGroupVO().getType(), Keys.TYPE_HTTP_CUSTOM)) {
|
||||
// /api/v1/public/getUserInfo.svc
|
||||
String extUrl = StringUtils.removeStart(request.getRequestURI(), request.getContextPath());
|
||||
prop.put(INBOUND_EXTURI, extUrl);
|
||||
} else {
|
||||
prop.put(INBOUND_EXTURI, getExtUri(request));
|
||||
}
|
||||
prop.put(Processor.REQUEST_ACTION, adptVO.getAdapterGroupVO().getRefClass());
|
||||
prop.put(API_PATH, httpProp.getProperty(API_PATH, ""));
|
||||
prop.put(PRE_FILTERS, httpProp.getProperty(PRE_FILTERS, ""));
|
||||
prop.put(POST_FILTERS, httpProp.getProperty(POST_FILTERS, ""));
|
||||
prop.put(PROPERTIES_NAME_HTTP_REQUEST_METHOD, request.getMethod());
|
||||
prop.put(ALLOW_IP, httpProp.getProperty(ALLOW_IP, ""));
|
||||
|
||||
isParameterType = false;
|
||||
try {
|
||||
traceLevel = Integer.parseInt(traceLevelTemp);
|
||||
} catch (Exception e) {
|
||||
traceLevel = 0;
|
||||
}
|
||||
|
||||
stopWatch = new StopWatch();
|
||||
stopWatch.start();
|
||||
|
||||
logger.debug("시작 >> encode = [" + encode + "]");
|
||||
|
||||
switch (HttpMethodType.getValue(request.getMethod())) {
|
||||
case GET:
|
||||
case DELETE:
|
||||
isParameterType = true;
|
||||
break;
|
||||
case POST:
|
||||
case PUT:
|
||||
if (StringUtils.contains(request.getContentType(), "application/x-www-form-urlencoded")) {
|
||||
isParameterType = true;
|
||||
} else {
|
||||
isParameterType = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (isParameterType) {
|
||||
paramValue = request.getQueryString();
|
||||
if (paramValue == null)
|
||||
paramValue = "";
|
||||
if (traceLevel >= 3) {
|
||||
HttpMemoryLogger.txlog(adptGrpName + adptName,
|
||||
"RECV " + "[" + paramValue + "]" + CommonLib.getDumpMessage(paramValue));
|
||||
}
|
||||
|
||||
// json으로 변환
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("{");
|
||||
Map<String, String[]> paramMap = assignParameterMap(request, adptGrpName, adptName, null, prop);
|
||||
int i = 0;
|
||||
for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
|
||||
if (i > 0) {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append("\"").append(entry.getKey()).append("\":");
|
||||
String[] values = entry.getValue();
|
||||
if (values.length > 1) {
|
||||
// ["111", "222"]
|
||||
sb.append("[");
|
||||
for (int j = 0; j < values.length; j++) {
|
||||
if (j > 0) {
|
||||
sb.append(",");
|
||||
}
|
||||
sb.append("\"").append(JSONValue.escape(values[j])).append("\"");
|
||||
}
|
||||
sb.append("]");
|
||||
} else {
|
||||
sb.append("\"").append(JSONValue.escape(values[0])).append("\"");
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
sb.append("}");
|
||||
|
||||
paramValue = sb.toString();
|
||||
} else {
|
||||
if (ServletFileUpload.isMultipartContent(request)) {
|
||||
// TODO : 아래의 로직은 업무에 맞게 수정이 필요함.
|
||||
// 불필요할 경우 제거
|
||||
// if(StringUtils.isEmpty(uploadRootPath)) {
|
||||
// uploadRootPath = System.getProperty("java.io.tmpdir");
|
||||
// }
|
||||
|
||||
// 임시로직 : UPLOAD_ROOT_PATH 가 없는 경우에는 JSON에 추가
|
||||
if (StringUtils.isEmpty(uploadRootPath)) {
|
||||
paramValue = readMultipartBody(request);
|
||||
} else {
|
||||
paramValue = uploadMultipartBody(request, uploadRootPath);
|
||||
}
|
||||
// TEST : 테스트용 임시코드
|
||||
// response.setCharacterEncoding(encode);
|
||||
// response.getWriter().print(paramValue);
|
||||
// return;
|
||||
} else {
|
||||
ServletInputStream sis = request.getInputStream();
|
||||
ByteBuffer bb = ByteBuffer.allocate(1024).setAutoExpand(true);
|
||||
int i = 0;
|
||||
byte[] cbuf = new byte[1024];
|
||||
while ((i = sis.read(cbuf, 0, 1024)) != -1) {
|
||||
if (i == 1024) {
|
||||
bb.put(cbuf);
|
||||
} else {
|
||||
byte[] tail = new byte[i];
|
||||
System.arraycopy(cbuf, 0, tail, 0, i);
|
||||
bb.put(tail);
|
||||
}
|
||||
}
|
||||
byte[] data = new byte[bb.position()];
|
||||
bb.position(0);
|
||||
bb.get(data);
|
||||
paramValue = new String(data, encode);
|
||||
|
||||
if (traceLevel >= 3) {
|
||||
HttpMemoryLogger.txlog(adptGrpName + adptName,
|
||||
"RECV " + "[" + paramValue + "]" + CommonLib.getDumpMessage(paramValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (paramValue == null) { // parameter가 없는 경우때문에 처리
|
||||
paramValue = "";
|
||||
}
|
||||
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("HttpAdapterServiceRest] RECV (" + adptGrpName + ") = [" + paramValue + "]\n"
|
||||
+ CommonLib.getDumpMessage(paramValue));
|
||||
}
|
||||
|
||||
if ("Y".equals(urlDecodeYn) && isParameterType) {
|
||||
message = URLDecoder.decode(paramValue);
|
||||
} else {
|
||||
message = paramValue;
|
||||
}
|
||||
|
||||
if (logger.isDebug()) {
|
||||
String[] msgArgs = new String[2];
|
||||
msgArgs[0] = adptGrpName;
|
||||
msgArgs[1] = message;
|
||||
String resMsg = ExceptionUtil.make("RICEAIAHA005", msgArgs);
|
||||
logger.debug(logPrefix + resMsg);
|
||||
}
|
||||
|
||||
adptMsgType = adptVO.getAdapterGroupVO().getMessageType();
|
||||
if (StringUtils.equals(adptMsgType, MessageType.JSON)) {
|
||||
response.setContentType(JSON_CONTENT_TYPE+"; charset="+encode);
|
||||
}
|
||||
|
||||
// HEADER_GROUP 셋팅
|
||||
if (MessageType.JSON.equals(adptMsgType) && StringUtils.isNotBlank(headerGroupName)
|
||||
&& StringUtils.isNotBlank(relayRequestHeaderKeys)) {
|
||||
JSONObject jsonMessage = (JSONObject) JSONValue.parse(message);
|
||||
JSONObject headerJson = new JSONObject();
|
||||
if (StringUtils.equalsIgnoreCase(relayRequestHeaderKeys, "ALL")) {
|
||||
for (Enumeration<String> e = request.getHeaderNames(); e.hasMoreElements();) {
|
||||
String key = e.nextElement();
|
||||
headerJson.put(key, request.getHeader(key));
|
||||
}
|
||||
} else {
|
||||
String[] relayKeyArr = org.springframework.util.StringUtils
|
||||
.tokenizeToStringArray(relayRequestHeaderKeys, ",");
|
||||
|
||||
for (String key : relayKeyArr) {
|
||||
String headerValue = request.getHeader(key);
|
||||
if (StringUtils.isNotBlank(headerValue)) {
|
||||
headerJson.put(key, headerValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (headerJson.size() > 0) {
|
||||
jsonMessage.put(headerGroupName, headerJson);
|
||||
message = jsonMessage.toJSONString();
|
||||
}
|
||||
}
|
||||
|
||||
if (message == null) {
|
||||
message = "";
|
||||
}
|
||||
|
||||
// com.eactive.eai.custom.adapter.http.dynamic.filter.VirtualAccountCryptoFilter
|
||||
prop = addCryptoFilter(prop);
|
||||
|
||||
// 로컬 서비스 호출 ,encoding 처리 추가
|
||||
String result = (String) service(adptGrpName, adptName, message, prop, request, response);
|
||||
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("HttpAdapterServiceRest] result " + encode + " (" + adptGrpName + ") = [" + result + "]");
|
||||
}
|
||||
|
||||
stopWatch.stop();
|
||||
|
||||
String responseData = "";
|
||||
if (RESPONSE_TYPE_ASYNC.equals(responseType)) {
|
||||
if (stopWatch.getTime() > slowTranTime && (logger.isInfo())) {
|
||||
logger.info("HttpAdapterServiceRest] dummy response time = " + stopWatch.toString() + ", message = "
|
||||
+ message);
|
||||
|
||||
}
|
||||
if (result == null) {
|
||||
responseData = ElinkConfig.getAsyncDummyDataForAdapterGroup(adptGrpName);
|
||||
}
|
||||
|
||||
response.setCharacterEncoding(encode);
|
||||
response.getWriter().print(responseData);
|
||||
if (traceLevel >= 3) {
|
||||
HttpMemoryLogger.txlog(adptGrpName + adptName,
|
||||
"SEND " + "[" + responseData + "]" + CommonLib.getDumpMessage(responseData));
|
||||
}
|
||||
} else {
|
||||
responseData = result;
|
||||
logger.info("종료 >> encode = [" + encode + "]");
|
||||
|
||||
JSONObject dataObject = null;
|
||||
if (MessageType.JSON.equals(adptMsgType)) {
|
||||
dataObject = (JSONObject) JSONValue.parse(responseData);
|
||||
}
|
||||
|
||||
// HEADER_GROUP 하위 필드를 response Header에 세팅한다.
|
||||
HashMap<String, String> header = new HashMap<>();
|
||||
boolean redirect = assignHttpHeaders(header, dataObject, headerGroupName);
|
||||
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("HttpAdapterServiceRest] response header field (" + adptGrpName + ") = ["
|
||||
+ header.toString() + "]");
|
||||
}
|
||||
|
||||
if (redirect) {
|
||||
response.setStatus(302);
|
||||
logger.debug("HttpAdapterServiceRest] set response status_code: 302");
|
||||
} else {
|
||||
String httpStatus = header.get(HTTP_STATUS);
|
||||
if (httpStatus != null && !"".equals(httpStatus))
|
||||
response.setStatus(Integer.parseInt(httpStatus));
|
||||
header.remove(HTTP_STATUS);
|
||||
}
|
||||
|
||||
// response header 셋팅
|
||||
for (Map.Entry<String, String> entry : header.entrySet()) {
|
||||
response.setHeader(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
if (dataObject != null) {
|
||||
responseData = dataObject.toJSONString();
|
||||
}
|
||||
|
||||
// UI와 통신시(UTF-8) 변환오류로 ENCODE 제거
|
||||
response.setCharacterEncoding(encode);
|
||||
response.getWriter().print(responseData);
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("HttpAdapterServiceRest] SEND (" + adptGrpName + ") = [" + responseData + "]");
|
||||
logger.debug("HttpAdapterServiceRest] SEND (" + adptGrpName + ") = "
|
||||
+ CommonLib.getDumpMessage(responseData));
|
||||
}
|
||||
if (traceLevel >= 3) {
|
||||
HttpMemoryLogger.txlog(adptGrpName + adptName,
|
||||
"SEND " + "[" + responseData + "]" + CommonLib.getDumpMessage(responseData));
|
||||
}
|
||||
}
|
||||
} catch (HttpStatusException e) {
|
||||
if (traceLevel >= 3) {
|
||||
HttpMemoryLogger.error(adptGrpName + adptName, e.toString(), e);
|
||||
}
|
||||
logger.warn("HttpAdapter] " + adptGrpName + "-" + adptName + ">>" + e.getMessage());
|
||||
response.setStatus(e.getStatus());
|
||||
try {
|
||||
String errorMsg = MessageUtil.makeErrorMessageByMessageType(adptMsgType, encode, e.getCode(),
|
||||
e.getMessage(), errorResponseFormat);
|
||||
response.getWriter().println(errorMsg);
|
||||
} catch (Exception ex) {
|
||||
// IGNORE
|
||||
}
|
||||
} catch (JwtAuthException e) {
|
||||
if (traceLevel >= 3) {
|
||||
HttpMemoryLogger.error(adptGrpName + adptName, e.toString(), e);
|
||||
}
|
||||
logger.error(logPrefix + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
|
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value());
|
||||
try {
|
||||
String errorMsg = MessageUtil.makeErrorMessageByMessageType(adptMsgType, encode, e.getCode(),
|
||||
e.getMessage(), errorResponseFormat);
|
||||
response.getWriter().println(errorMsg);
|
||||
} catch (Exception ex) {
|
||||
// IGNORE
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (traceLevel >= 3) {
|
||||
HttpMemoryLogger.error(adptGrpName + adptName, e.toString(), e);
|
||||
}
|
||||
logger.error(logPrefix + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
|
||||
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
|
||||
try {
|
||||
response.getWriter().println(e.getMessage());
|
||||
String errCode = ExceptionUtil.getErrorCode(e, "RECEAIAHA003");
|
||||
throw new Exception(errCode);
|
||||
} catch (Exception ex) {
|
||||
// IGNORE
|
||||
logger.warn(logPrefix + adptGrpName + "-" + adptName + ">>" + e.getMessage(), e);
|
||||
}
|
||||
} finally {
|
||||
String uuid = prop.getProperty(TransactionContextKeys.TRANSACTION_UUID);
|
||||
String url = prop.getProperty(HttpClientAdapterServiceKey.INBOUND_EXTURI);
|
||||
String method = prop.getProperty(HttpClientAdapterServiceKey.INBOUND_METHOD);
|
||||
String adapterGroupName = prop.getProperty(HttpClientAdapterServiceKey.ADAPTER_GROUP_NAME);
|
||||
String adapterName = prop.getProperty(HttpClientAdapterServiceKey.ADAPTER_NAME);
|
||||
int httpStatusCode = response.getStatus();
|
||||
HttpAdapterExtraLogUtil.insertHttpAdapterExtraLog(uuid, 400, adapterGroupName, adapterName, new HashMap<>(), url, method, httpStatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String[]> assignParameterMap(HttpServletRequest request, String adptGrpName, String adptName,
|
||||
Object requestBytes, Properties prop) {
|
||||
// PathVariable 체크
|
||||
if (StringUtils.equalsAnyIgnoreCase(request.getMethod(), HttpMethod.GET.name(), HttpMethod.DELETE.name())
|
||||
&& StringUtils.isBlank(request.getQueryString())) {
|
||||
try {
|
||||
String actionName = prop.getProperty(Processor.REQUEST_ACTION);
|
||||
RequestAction action = ActionFactory.createAction(actionName);
|
||||
action.setAdapterInfo(adptGrpName, adptName, prop);
|
||||
String[] keys = action.perform(requestBytes);
|
||||
String requestPath = keys[0];
|
||||
|
||||
// PathVariable 지원 추가
|
||||
String ruledPath = StandardMessageUtil.getMatchedKey(requestPath, actionName);
|
||||
if (!StringUtils.equals(requestPath, ruledPath) && StringUtils.contains(ruledPath, "{")) {
|
||||
Map<String, String> paramMap = new AntPathMatcher().extractUriTemplateVariables(ruledPath,
|
||||
requestPath);
|
||||
if (paramMap != null && paramMap.size() > 0) {
|
||||
Map<String, String[]> returnMap = new HashMap<>();
|
||||
for (String key : paramMap.keySet()) {
|
||||
if (StringUtils.equalsIgnoreCase(key, "method")) {
|
||||
continue;
|
||||
}
|
||||
returnMap.put(key, new String[] { paramMap.get(key) });
|
||||
}
|
||||
|
||||
return returnMap;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return request.getParameterMap();
|
||||
}
|
||||
|
||||
private void validateServiceAndAdapter(String adptGrpName, String adptName, byte[] requestBytes, Properties prop)
|
||||
throws JwtAuthException {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* request header 를 hashmap으로 조립
|
||||
*
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
private Properties getHeaders(HttpServletRequest request) {
|
||||
Properties prop = new Properties();
|
||||
|
||||
Enumeration<String> headerNames = request.getHeaderNames();
|
||||
while (headerNames.hasMoreElements()) {
|
||||
String key = headerNames.nextElement();
|
||||
String value = request.getHeader(key);
|
||||
prop.setProperty(key, value);
|
||||
}
|
||||
|
||||
return prop;
|
||||
}
|
||||
|
||||
/**
|
||||
* adapter property의 HEADER_GROUP으로 정의된 (MFE_HEADER) 그룹의 하위 필드를 http Header에
|
||||
* 세팅한다.
|
||||
*
|
||||
* @param header
|
||||
* @param object
|
||||
*/
|
||||
private boolean assignHttpHeaders(HashMap<String, String> header, Object msg, String headerGroupName) {
|
||||
boolean redirect = false;
|
||||
|
||||
if (msg == null || StringUtils.isBlank(headerGroupName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (msg instanceof JSONObject) {
|
||||
JSONObject headerObject = (JSONObject) ((JSONObject) msg).get(headerGroupName);
|
||||
|
||||
if (headerObject == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// List<String> headerKeys = new ArrayList<>();
|
||||
for (Object key : headerObject.keySet()) {
|
||||
|
||||
Object obj = headerObject.get(key);
|
||||
if ((obj instanceof JSONObject) || (obj instanceof JSONArray)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
header.put((String) key, (String) obj);
|
||||
|
||||
if (StringUtils.equalsIgnoreCase((String) key, "Location")) {
|
||||
redirect = true;
|
||||
}
|
||||
}
|
||||
|
||||
((JSONObject) msg).remove(headerGroupName);
|
||||
}
|
||||
|
||||
return redirect;
|
||||
}
|
||||
|
||||
/**
|
||||
* 어댑터 명 이후의 URI값을 가져온다.
|
||||
*
|
||||
* @param request
|
||||
* @return
|
||||
*/
|
||||
private String getExtUri(HttpServletRequest request) {
|
||||
String orgUri = request.getRequestURI().replaceAll(request.getContextPath(), "");
|
||||
String uri = getExtUri(orgUri, 3);
|
||||
if (uri != null && uri.trim().length() > 0) {
|
||||
return "/" + uri;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static String getExtUri(String url, int length) {
|
||||
String[] urls = url.split("/");
|
||||
List<String> newUrls = new ArrayList<>();
|
||||
Collections.addAll(newUrls, urls);
|
||||
return StringUtils.join(newUrls.subList(length, urls.length).toArray(), "/");
|
||||
}
|
||||
|
||||
// public static void main(String[] args) throws Exception {
|
||||
// String orgUri = "/HTT/CbsInNetSys/abcd/123456";
|
||||
// String result = "";
|
||||
// result = getExtUri(orgUri, 3);
|
||||
// System.out.println(result);
|
||||
// }
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
package com.eactive.eai.adapter.http.filter;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.common.security.CryptoModuleService;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.util.JsonPathUtil;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
/**
|
||||
* InCryptoFilter / OutCryptoFilter 공통 기반 클래스.
|
||||
*
|
||||
* BODY/FIELD 범위 암복호화 로직, 프로퍼티 상수, 유틸리티 메서드를 제공한다.
|
||||
* HTTP 서버 어댑터용은 {@link InCryptoFilter}, 클라이언트 어댑터용은 {@link OutCryptoFilter} 참조.
|
||||
*
|
||||
* 프로퍼티 키:
|
||||
* CRYPTO_MODULE_NAME 필수. DB에 등록된 암호화 모듈명.
|
||||
* CRYPTO_SCOPE BODY | FIELD (기본값: FIELD)
|
||||
* BODY : 메시지 전체를 암복호화 (Base64 인코딩/디코딩)
|
||||
* FIELD : 특정 JSON 경로의 값만 암복호화
|
||||
*
|
||||
* FIELD 범위 전용:
|
||||
* CRYPTO_DEC_FROM_PATH 복호화 대상 JSON 경로 (기본값: /encrypted_data)
|
||||
* CRYPTO_DEC_TO_PATH 복호화 결과 반영 경로. "/" = 전체 body 교체.
|
||||
* CRYPTO_ENC_FROM_PATH 암호화 대상 JSON 경로. "/" = 전체 body 암호화.
|
||||
* CRYPTO_ENC_TO_PATH 암호화 결과(Base64)를 넣을 JSON 경로 (기본값: /encrypted_data)
|
||||
*
|
||||
* GCM AAD 전용 (선택):
|
||||
* CRYPTO_AAD_HEADER AAD로 사용할 HTTP 요청 헤더명 (InCryptoFilter 전용).
|
||||
* 미설정 또는 헤더값 없으면 aad=null → IV를 AAD 대체값으로 사용.
|
||||
*/
|
||||
public abstract class AbstractCryptoFilter {
|
||||
|
||||
protected static Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
public static final String PROP_MODULE_NAME = "CRYPTO_MODULE_NAME";
|
||||
public static final String PROP_SCOPE = "CRYPTO_SCOPE";
|
||||
public static final String PROP_DEC_FROM_PATH = "CRYPTO_DEC_FROM_PATH";
|
||||
public static final String PROP_DEC_TO_PATH = "CRYPTO_DEC_TO_PATH";
|
||||
public static final String PROP_ENC_FROM_PATH = "CRYPTO_ENC_FROM_PATH";
|
||||
public static final String PROP_ENC_TO_PATH = "CRYPTO_ENC_TO_PATH";
|
||||
public static final String PROP_AAD_HEADER = "CRYPTO_AAD_HEADER";
|
||||
|
||||
public static final String SCOPE_BODY = "BODY";
|
||||
public static final String SCOPE_FIELD = "FIELD";
|
||||
public static final String PATH_ROOT = "/";
|
||||
public static final String PATH_ENCDATA = "/encrypted_data";
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 복호화 구현
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
protected String decryptBody(String body, String moduleName, Map<String, String> runtimeCtx,
|
||||
byte[] aad) throws Exception {
|
||||
byte[] cipherBytes = Base64.getDecoder().decode(body.trim());
|
||||
byte[] plainBytes = CryptoModuleService.getInstance().decrypt(moduleName, runtimeCtx, aad, cipherBytes);
|
||||
return new String(plainBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* fromPath의 Base64 값을 복호화하여 toPath에 반영. JSON 파싱 1회.
|
||||
* toPath = "/" → fromPath 필드를 제거하고 복호화된 JSON을 body에 병합.
|
||||
*/
|
||||
protected Object decryptField(String body, String moduleName, Map<String, String> runtimeCtx,
|
||||
byte[] aad, String fromPath, String toPath) throws Exception {
|
||||
JsonNode root = JsonPathUtil.toTree(body);
|
||||
String encBase64 = JsonPathUtil.getAt(root, fromPath);
|
||||
if (StringUtils.isBlank(encBase64)) {
|
||||
logger.warn("CryptoFilter] 복호화 대상 필드 없음: path=" + fromPath);
|
||||
return body;
|
||||
}
|
||||
byte[] cipherBytes = Base64.getDecoder().decode(encBase64.trim());
|
||||
byte[] plainBytes = CryptoModuleService.getInstance().decrypt(moduleName, runtimeCtx, aad, cipherBytes);
|
||||
String plainText = new String(plainBytes, StandardCharsets.UTF_8);
|
||||
if (PATH_ROOT.equals(toPath)) {
|
||||
return JsonPathUtil.mergeAtRoot(root, fromPath, plainText);
|
||||
}
|
||||
JsonPathUtil.setAt(root, toPath, plainText);
|
||||
return root;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 암호화 구현
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
protected String encryptBody(String body, String moduleName, Map<String, String> runtimeCtx,
|
||||
byte[] aad) throws Exception {
|
||||
byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx, aad,
|
||||
body.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(cipherBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* fromPath 값을 암호화하여 toPath(Base64)에 반영. JSON 파싱 1회.
|
||||
* fromPath = "/" → body 전체를 암호화하여 toPath 필드명으로 래핑 (파싱 불필요).
|
||||
*/
|
||||
protected Object encryptField(String body, String moduleName, Map<String, String> runtimeCtx,
|
||||
byte[] aad, String fromPath, String toPath) throws Exception {
|
||||
if (PATH_ROOT.equals(fromPath)) {
|
||||
byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx, aad,
|
||||
body.getBytes(StandardCharsets.UTF_8));
|
||||
String encBase64 = Base64.getEncoder().encodeToString(cipherBytes);
|
||||
String fieldName = toPath.replaceFirst("^/", "");
|
||||
return "{\"" + fieldName + "\":\"" + encBase64 + "\"}";
|
||||
}
|
||||
JsonNode root = JsonPathUtil.toTree(body);
|
||||
String plainText = JsonPathUtil.getAt(root, fromPath);
|
||||
if (StringUtils.isBlank(plainText)) {
|
||||
logger.warn("CryptoFilter] 암호화 대상 필드 없음: path=" + fromPath);
|
||||
return root;
|
||||
}
|
||||
byte[] cipherBytes = CryptoModuleService.getInstance().encrypt(moduleName, runtimeCtx, aad,
|
||||
plainText.getBytes(StandardCharsets.UTF_8));
|
||||
String encBase64 = Base64.getEncoder().encodeToString(cipherBytes);
|
||||
JsonPathUtil.setAt(root, toPath, encBase64);
|
||||
return root;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// 유틸
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 필터로 전달되는 message 객체를 JSON 문자열로 변환한다.
|
||||
*/
|
||||
protected String toBodyString(Object message) {
|
||||
if (message instanceof String) {
|
||||
return (String) message;
|
||||
}
|
||||
if (message instanceof byte[]) {
|
||||
return new String((byte[]) message, StandardCharsets.UTF_8);
|
||||
}
|
||||
return message.toString();
|
||||
}
|
||||
|
||||
protected String required(Properties prop, String key) {
|
||||
String v = prop.getProperty(key);
|
||||
if (StringUtils.isBlank(v)) {
|
||||
throw new IllegalArgumentException("CryptoFilter 필수 프로퍼티 누락: " + key);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
protected String getProp(Properties prop, String key, String defaultVal) {
|
||||
String v = prop.getProperty(key);
|
||||
return StringUtils.isBlank(v) ? defaultVal : v;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.eactive.eai.agent.encryption;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.eactive.eai.common.exception.ExceptionUtil;
|
||||
@@ -12,8 +11,6 @@ import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.server.Keys;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.ext.djb.DamoManager;
|
||||
import com.eactive.ext.kjb.safedb.KjbSafedbWrapper;
|
||||
|
||||
@Component
|
||||
public class EncryptionManager implements Lifecycle {
|
||||
@@ -21,8 +18,6 @@ public class EncryptionManager implements Lifecycle {
|
||||
private static final String GROUP_NAME = "ENCRYPTION";
|
||||
|
||||
private String encryptYN = "";
|
||||
private String dbEncryptSolutionName = "DAMO";
|
||||
|
||||
private boolean started;
|
||||
private LifecycleSupport lifecycle = new LifecycleSupport(this);
|
||||
|
||||
@@ -60,15 +55,8 @@ public class EncryptionManager implements Lifecycle {
|
||||
encryptYN = "N";
|
||||
}
|
||||
}
|
||||
dbEncryptSolutionName = PropManager.getInstance().getProperty(GROUP_NAME, "dbEncryptSolutionName");
|
||||
if (dbEncryptSolutionName == null) {
|
||||
dbEncryptSolutionName = "DAMO";
|
||||
} else {
|
||||
if (dbEncryptSolutionName.trim().length() == 0) {
|
||||
dbEncryptSolutionName = "DAMO";
|
||||
}
|
||||
}
|
||||
logger.warn("EncryptionManager] init :: encryption YN [ " + encryptYN + " ], dbEncryptSolutionName [ " + dbEncryptSolutionName + " ]");
|
||||
|
||||
logger.warn("EncryptionManager] init :: encryption YN [ " + encryptYN + " ]");
|
||||
}
|
||||
|
||||
public synchronized void reload() throws Exception {
|
||||
@@ -111,122 +99,4 @@ public class EncryptionManager implements Lifecycle {
|
||||
public String getEncryptYN() {
|
||||
return encryptYN;
|
||||
}
|
||||
|
||||
public String getDBEncryptSolutionName() {
|
||||
return dbEncryptSolutionName;
|
||||
}
|
||||
|
||||
public boolean isEncrypt() {
|
||||
return "Y".equalsIgnoreCase(encryptYN);
|
||||
}
|
||||
|
||||
public String encryptDBData(String plainText) {
|
||||
if(isEncrypt()) {
|
||||
if ("DAMO".equals(dbEncryptSolutionName)) {
|
||||
com.eactive.ext.djb.DamoManager damoManager = new com.eactive.ext.djb.DamoManager();
|
||||
return damoManager.encrypt(plainText);
|
||||
} else if ("SAFEDB".equals(dbEncryptSolutionName)) {
|
||||
String originalAttribute = plainText;
|
||||
if (StringUtils.isNotEmpty(plainText)) {
|
||||
KjbSafedbWrapper wrapper = KjbSafedbWrapper.getInstance();
|
||||
plainText = wrapper.encryptNotRnnoString(plainText);
|
||||
|
||||
if (logger.isInfo()) {
|
||||
// originalAttribute 홀수 글자 마스킹
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < originalAttribute.length(); i++) {
|
||||
if (i % 2 == 0) {
|
||||
sb.append(originalAttribute.charAt(i));
|
||||
} else {
|
||||
sb.append("*");
|
||||
}
|
||||
}
|
||||
|
||||
originalAttribute = sb.toString();
|
||||
// System.out.println("DB 암호화 #2 : {"+ originalAttribute +"} ->
|
||||
// {"+attribute+"}");
|
||||
// logger.debug("DB 암호화 #2 : {} -> {}", originalAttribute, attribute);
|
||||
}
|
||||
}
|
||||
return plainText;
|
||||
} else {
|
||||
return plainText;
|
||||
}
|
||||
} else {
|
||||
return plainText;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public String decryptDBData(String dbData) {
|
||||
if (StringUtils.isNotEmpty(dbData)) {
|
||||
if (this.isEncrypted(dbData)) {
|
||||
if ("DAMO".equals(dbEncryptSolutionName)) {
|
||||
com.eactive.ext.djb.DamoManager damoManager = new com.eactive.ext.djb.DamoManager();
|
||||
try {
|
||||
return damoManager.decrypt(dbData);
|
||||
} catch (Exception e) {
|
||||
// 복호화 실패시 원본 반환
|
||||
String logData = dbData.length() <= 3 ? dbData : dbData.substring(0, 3) + "...";
|
||||
logger.warn("DB 복호화 실패: 복호화하지 않고 원본 반환. dbData={} (Length : {})", logData, dbData.length());
|
||||
return dbData;
|
||||
}
|
||||
} else if ("SAFEDB".equals(dbEncryptSolutionName)) {
|
||||
KjbSafedbWrapper safeDBWrapper = KjbSafedbWrapper.getInstance();
|
||||
try {
|
||||
dbData = safeDBWrapper.decryptNotRnno(dbData);
|
||||
} catch (Exception e) {
|
||||
// 복호화 실패시 원본 반환
|
||||
String logData = dbData.length() <= 3 ? dbData : dbData.substring(0, 3) + "...";
|
||||
logger.warn("DB 복호화 실패: 복호화하지 않고 원본 반환. dbData={} (Length : {})", logData, dbData.length());
|
||||
return dbData;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.debug("DB 복호화 경고: Base64 형식이 아님. 복호화하지 않고 원본 반환. dbData={}", dbData);
|
||||
}
|
||||
}
|
||||
// logger.debug("DB 복호화 #2 : {} -> {}", dbDataOriginal, dbData);
|
||||
return dbData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 암호화 여부 확인(base64 인코딩 여부로 판단, 또는 0-9, - 로 구성된 경우 암호화 되지 않은 걸로 판단(연락처))
|
||||
*
|
||||
* @param data
|
||||
* @return
|
||||
*/
|
||||
private boolean isEncrypted(String data) {
|
||||
if (StringUtils.isEmpty(data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 숫자, - 로만 구성된 경우 암호화 되지 않은 걸로 판단 (ex: 연락처)
|
||||
if (data.matches("^[0-9-]+$")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
data = data.trim();
|
||||
|
||||
// Base64 형식 체크
|
||||
if (!isBase64(data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Base64 길이 체크 (4의 배수)
|
||||
if (data.length() % 4 != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isBase64(String str) {
|
||||
if (str == null || str.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String base64Pattern = "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$";
|
||||
return str.matches(base64Pattern);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@ package com.eactive.eai.agent.inflow;
|
||||
|
||||
import com.eactive.eai.agent.command.Command;
|
||||
import com.eactive.eai.agent.command.CommandException;
|
||||
import com.eactive.eai.common.inflow.AbstractInflowControlManager;
|
||||
import com.eactive.eai.common.inflow.InflowControlUtil;
|
||||
import com.eactive.eai.common.inflow.InflowControlManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class ReloadInflowAdapterControlCommand extends Command {
|
||||
@@ -28,7 +27,7 @@ public class ReloadInflowAdapterControlCommand extends Command {
|
||||
|
||||
String keyName = (String) args;
|
||||
try {
|
||||
AbstractInflowControlManager manager = InflowControlUtil.getInflowControlManager();
|
||||
InflowControlManager manager = InflowControlManager.getInstance();
|
||||
|
||||
if (keyName != null) {
|
||||
if ("ALL".equals(keyName)) {
|
||||
|
||||
@@ -2,8 +2,7 @@ package com.eactive.eai.agent.inflow;
|
||||
|
||||
import com.eactive.eai.agent.command.Command;
|
||||
import com.eactive.eai.agent.command.CommandException;
|
||||
import com.eactive.eai.common.inflow.AbstractInflowControlManager;
|
||||
import com.eactive.eai.common.inflow.InflowControlUtil;
|
||||
import com.eactive.eai.common.inflow.InflowControlManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class ReloadInflowGroupControlCommand extends Command {
|
||||
@@ -29,7 +28,7 @@ public class ReloadInflowGroupControlCommand extends Command {
|
||||
|
||||
String keyName = (String) args;
|
||||
try {
|
||||
AbstractInflowControlManager manager = InflowControlUtil.getInflowControlManager();
|
||||
InflowControlManager manager = InflowControlManager.getInstance();
|
||||
|
||||
if (keyName != null) {
|
||||
if ("ALL".equals(keyName)) {
|
||||
|
||||
@@ -2,8 +2,7 @@ package com.eactive.eai.agent.inflow;
|
||||
|
||||
import com.eactive.eai.agent.command.Command;
|
||||
import com.eactive.eai.agent.command.CommandException;
|
||||
import com.eactive.eai.common.inflow.AbstractInflowControlManager;
|
||||
import com.eactive.eai.common.inflow.InflowControlUtil;
|
||||
import com.eactive.eai.common.inflow.InflowControlManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class ReloadInflowInterfaceControlCommand extends Command {
|
||||
@@ -29,7 +28,7 @@ public class ReloadInflowInterfaceControlCommand extends Command {
|
||||
|
||||
String keyName = (String) args;
|
||||
try {
|
||||
AbstractInflowControlManager manager = InflowControlUtil.getInflowControlManager();
|
||||
InflowControlManager manager = InflowControlManager.getInstance();
|
||||
|
||||
if (keyName != null) {
|
||||
if ("ALL".equals(keyName)) {
|
||||
|
||||
@@ -2,8 +2,7 @@ package com.eactive.eai.agent.inflow;
|
||||
|
||||
import com.eactive.eai.agent.command.Command;
|
||||
import com.eactive.eai.agent.command.CommandException;
|
||||
import com.eactive.eai.common.inflow.AbstractInflowControlManager;
|
||||
import com.eactive.eai.common.inflow.InflowControlUtil;
|
||||
import com.eactive.eai.common.inflow.InflowControlManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class RemoveInflowAdapterControlCommand extends Command {
|
||||
@@ -27,7 +26,7 @@ public class RemoveInflowAdapterControlCommand extends Command {
|
||||
|
||||
String key = (String) args;
|
||||
try {
|
||||
AbstractInflowControlManager manager = InflowControlUtil.getInflowControlManager();
|
||||
InflowControlManager manager = InflowControlManager.getInstance();
|
||||
manager.removeAdapter(key);
|
||||
} catch (Exception e) {
|
||||
String rspErrorCode = "RECEAIMCM002";
|
||||
|
||||
@@ -2,8 +2,7 @@ package com.eactive.eai.agent.inflow;
|
||||
|
||||
import com.eactive.eai.agent.command.Command;
|
||||
import com.eactive.eai.agent.command.CommandException;
|
||||
import com.eactive.eai.common.inflow.AbstractInflowControlManager;
|
||||
import com.eactive.eai.common.inflow.InflowControlUtil;
|
||||
import com.eactive.eai.common.inflow.InflowControlManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class RemoveInflowGroupControlCommand extends Command {
|
||||
@@ -29,7 +28,7 @@ public class RemoveInflowGroupControlCommand extends Command {
|
||||
|
||||
String key = (String) args;
|
||||
try {
|
||||
AbstractInflowControlManager manager = InflowControlUtil.getInflowControlManager();
|
||||
InflowControlManager manager = InflowControlManager.getInstance();
|
||||
manager.removeGroup(key);
|
||||
if (logger.isWarn())
|
||||
logger.warn(this.name + "] group " + key + " removed.");
|
||||
|
||||
@@ -2,8 +2,7 @@ package com.eactive.eai.agent.inflow;
|
||||
|
||||
import com.eactive.eai.agent.command.Command;
|
||||
import com.eactive.eai.agent.command.CommandException;
|
||||
import com.eactive.eai.common.inflow.AbstractInflowControlManager;
|
||||
import com.eactive.eai.common.inflow.InflowControlUtil;
|
||||
import com.eactive.eai.common.inflow.InflowControlManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class RemoveInflowInterfaceControlCommand extends Command {
|
||||
@@ -26,7 +25,7 @@ public class RemoveInflowInterfaceControlCommand extends Command {
|
||||
|
||||
String key = (String) args;
|
||||
try {
|
||||
AbstractInflowControlManager manager = InflowControlUtil.getInflowControlManager();
|
||||
InflowControlManager manager = InflowControlManager.getInstance();
|
||||
manager.removeInterface(key);
|
||||
} catch (Exception e) {
|
||||
String rspErrorCode = "RECEAIMCM002";
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.eactive.eai.agent.security;
|
||||
|
||||
import com.eactive.eai.agent.command.Command;
|
||||
import com.eactive.eai.agent.command.CommandException;
|
||||
import com.eactive.eai.common.security.CryptoModuleManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class ReloadCryptoModuleCommand extends Command {
|
||||
|
||||
private static final long serialVersionUID = 1032982004418318100L;
|
||||
|
||||
@Override
|
||||
public Object execute() throws CommandException {
|
||||
Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
|
||||
try {
|
||||
String cryptoId = (String) args;
|
||||
CryptoModuleManager.getInstance().reload(cryptoId);
|
||||
return "success";
|
||||
} catch (Exception e) {
|
||||
String rspErrorCode = "RECEAIMCM002";
|
||||
String msg = this.makeException(rspErrorCode, e);
|
||||
if (logger.isError()) {
|
||||
logger.error(msg, e);
|
||||
}
|
||||
throw new CommandException(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-6
@@ -173,12 +173,7 @@ public class HttpClientAccessTokenServiceWithBase64Header implements HttpClientA
|
||||
cmBuilder.setSSLSocketFactory(sslSocketFactory);
|
||||
} else {
|
||||
sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build();
|
||||
SSLConnectionSocketFactory csf = null;
|
||||
if (testMode) {
|
||||
csf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
|
||||
} else {
|
||||
csf = new SSLConnectionSocketFactory(sslContext);
|
||||
}
|
||||
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext);
|
||||
cmBuilder.setSSLSocketFactory(csf);
|
||||
}
|
||||
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException | CertificateException
|
||||
|
||||
+1
-6
@@ -181,12 +181,7 @@ public class HttpClientAccessTokenServiceWithBase64NiceOn implements HttpClientA
|
||||
cmBuilder.setSSLSocketFactory(sslSocketFactory);
|
||||
} else {
|
||||
sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build();
|
||||
SSLConnectionSocketFactory csf = null;
|
||||
if (testMode) {
|
||||
csf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
|
||||
} else {
|
||||
csf = new SSLConnectionSocketFactory(sslContext);
|
||||
}
|
||||
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext);
|
||||
cmBuilder.setSSLSocketFactory(csf);
|
||||
}
|
||||
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException | CertificateException
|
||||
|
||||
-911
@@ -1,911 +0,0 @@
|
||||
|
||||
package com.eactive.eai.authoutbound.client.impl;
|
||||
|
||||
import com.eactive.eai.adapter.AdapterGroupVO;
|
||||
import com.eactive.eai.adapter.AdapterManager;
|
||||
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey;
|
||||
import com.eactive.eai.adapter.http.secure.HttpClient5SSLContextFactory;
|
||||
import com.eactive.eai.authoutbound.OutboundOAuthCredentialVo;
|
||||
import com.eactive.eai.authoutbound.client.HttpClientAccessTokenServiceByDB;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.JacksonUtil;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.httpouttlsinfo.HttpOutTlsInfoManager;
|
||||
import com.eactive.eai.httpouttlsinfo.HttpOutTlsInfoVO;
|
||||
import com.eactive.eai.util.TestModeChecker;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.openbanking.eai.common.token.AccessTokenVO;
|
||||
import com.openbanking.eai.common.token.OAuth2AccessTokenVO;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpGet;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpPost;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
|
||||
import org.apache.hc.client5.http.config.RequestConfig;
|
||||
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClients;
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
|
||||
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
|
||||
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
|
||||
import org.apache.hc.client5.http.ssl.TrustAllStrategy;
|
||||
import org.apache.hc.core5.http.HttpEntity;
|
||||
import org.apache.hc.core5.http.HttpHost;
|
||||
import org.apache.hc.core5.http.NameValuePair;
|
||||
import org.apache.hc.core5.http.io.entity.EntityUtils;
|
||||
import org.apache.hc.core5.http.io.entity.StringEntity;
|
||||
import org.apache.hc.core5.http.message.BasicNameValuePair;
|
||||
import org.apache.hc.core5.net.URIBuilder;
|
||||
import org.apache.hc.core5.ssl.SSLContextBuilder;
|
||||
import org.apache.hc.core5.util.TimeValue;
|
||||
import org.apache.hc.core5.util.Timeout;
|
||||
import org.springframework.security.web.util.UrlUtils;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 1. 기능 : 설정 기반 범용 OAuth AccessToken 발급 구현체.
|
||||
*
|
||||
* 2. 처리 개요 :
|
||||
* - 토큰 발급에 필요한 <b>값</b>(client_id/secret/scope/grant_type/URL)은 기존과 동일하게 DB(OutboundOAuthCredentialVo)에서 읽고,
|
||||
* 사이트마다 달라지는 <b>형태</b>(파라미터명, 자격증명 전달 위치, 응답 구조)는 PropManager 의
|
||||
* "{@value #PROP_GROUP}" 프로퍼티 그룹에서 어댑터그룹 단위로 읽어 조립한다.
|
||||
* - 프로퍼티를 하나도 설정하지 않으면 HttpClientAccessTokenServiceWithDefault 와 동일하게 동작한다.
|
||||
*
|
||||
* <pre>
|
||||
* {어댑터그룹명}.content-type = application/x-www-form-urlencoded (기본값. VO 의 contentType 보다 우선)
|
||||
* {어댑터그룹명}.content-type.charset = Y (N 이면 Content-Type 에 charset 을 붙이지 않음)
|
||||
* {어댑터그룹명}.method = POST (POST | GET)
|
||||
*
|
||||
* # 자격증명 전달 방식
|
||||
* {어댑터그룹명}.credential.location = body | basic-header | custom-header (기본값 body)
|
||||
* {어댑터그룹명}.credential.header.name = Authorization (basic-header 기본값. custom-header 는 필수)
|
||||
* {어댑터그룹명}.credential.header.prefix = Basic (basic-header 기본값)
|
||||
* {어댑터그룹명}.credential.header.value = basic | client-id | client-secret (헤더에 담을 값. basic-header 는 basic, custom-header 는 client-secret 기본)
|
||||
* {어댑터그룹명}.credential.header.client-id.name = X-CLIENT-ID (client_id 도 별도 헤더로 보낼 때만)
|
||||
*
|
||||
* # 요청 필드명 매핑 (form 이면 파라미터명, json 이면 필드명, 헤더로 보내면 헤더명. json 은 dot-path 로 중첩 가능)
|
||||
* # 값을 none 으로 두면 그 필드는 전송하지 않는다. 전송할 파라미터가 하나도 없으면 바디 자체를 붙이지 않는다.
|
||||
* # in-header 에 나열한 필드는 바디가 아니라 헤더로 보낸다. (4개를 모두 나열하면 바디 없이 헤더로만 인증)
|
||||
* {어댑터그룹명}.request.fields.in-header = client-id,client-secret,scope,grant-type
|
||||
* {어댑터그룹명}.request.field.client-id = client_id
|
||||
* {어댑터그룹명}.request.field.client-secret = client_secret
|
||||
* {어댑터그룹명}.request.field.grant-type = grant_type
|
||||
* {어댑터그룹명}.request.field.scope = scope
|
||||
*
|
||||
* # 고정 추가 헤더 / 바디 (prefix 스캔으로 N 건)
|
||||
* {어댑터그룹명}.header.X-API-KEY = abcd
|
||||
* {어댑터그룹명}.body.institution_code = 0088
|
||||
*
|
||||
* # 응답 필드명 매핑 (dot-path 로 중첩 조회)
|
||||
* {어댑터그룹명}.response.field.access-token = dataBody.access_token
|
||||
* {어댑터그룹명}.response.field.token-type = token_type
|
||||
* {어댑터그룹명}.response.field.expires-in = expires_in
|
||||
* {어댑터그룹명}.response.field.scope = scope
|
||||
* {어댑터그룹명}.response.expires-in.default = 3600 (응답에 expires_in 이 없을 때. 미설정이면 VO 의 intervalSec, 그것도 없으면 3600)
|
||||
*
|
||||
* # HttpClient 옵션
|
||||
* {어댑터그룹명}.http.content-compression = Y (N 이면 Accept-Encoding: gzip 을 붙이지 않음)
|
||||
* {어댑터그룹명}.http.default-user-agent = Y (N 이면 기본 User-Agent 를 보내지 않음)
|
||||
*
|
||||
* # 응답 성공여부 판정 (설정한 경우에만 검사)
|
||||
* {어댑터그룹명}.response.success.field = dataHeader.GW_RSLT_CD
|
||||
* {어댑터그룹명}.response.success.value = 0000
|
||||
* </pre>
|
||||
*
|
||||
* 3. 주의사항
|
||||
* - client_secret 등 비밀정보는 절대 PropManager 에 설정하지 않는다. 관리포털에서 평문 조회되는 영역이다.
|
||||
* 값은 DB(VO), 형태만 프로퍼티라는 경계를 지킨다.
|
||||
* - HttpClientAccessTokenServiceFactoryByDB 가 className 기준으로 인스턴스를 캐싱하므로 이 클래스는
|
||||
* 상태를 가지면 안 된다. 프로퍼티는 반드시 execute() 안에서 매번 읽는다.
|
||||
*
|
||||
* @author :
|
||||
* @version : v 1.0.0
|
||||
* @see : HttpClientAccessTokenServiceWithDefault.java
|
||||
* @since :
|
||||
*/
|
||||
public class HttpClientAccessTokenServiceWithConfig implements HttpClientAccessTokenServiceByDB {
|
||||
|
||||
public static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
|
||||
|
||||
private static boolean testMode = TestModeChecker.isTestMode();
|
||||
|
||||
/** 어댑터그룹별 토큰 발급 형태를 조회할 프로퍼티 그룹 이름 */
|
||||
static final String PROP_GROUP = "OAuthTokenClient";
|
||||
|
||||
private static final String CONTENT_TYPE_FORM = "application/x-www-form-urlencoded";
|
||||
|
||||
private static final String LOCATION_BODY = "body";
|
||||
private static final String LOCATION_BASIC_HEADER = "basic-header";
|
||||
private static final String LOCATION_CUSTOM_HEADER = "custom-header";
|
||||
|
||||
private static final String SOURCE_BASIC = "basic";
|
||||
private static final String SOURCE_CLIENT_ID = "client-id";
|
||||
private static final String SOURCE_CLIENT_SECRET = "client-secret";
|
||||
|
||||
/** request.field.* / response.field.* 에 이 값을 주면 해당 필드를 사용하지 않는다. */
|
||||
private static final String FIELD_NONE = "none";
|
||||
|
||||
/** request.fields.in-header 에 나열할 수 있는 필드 이름 */
|
||||
private static final String FIELD_CLIENT_ID = "client-id";
|
||||
private static final String FIELD_CLIENT_SECRET = "client-secret";
|
||||
private static final String FIELD_GRANT_TYPE = "grant-type";
|
||||
private static final String FIELD_SCOPE = "scope";
|
||||
|
||||
private static final long DEFAULT_EXPIRES_IN = 3600L;
|
||||
|
||||
/** 캐시한 HttpClient 의 유휴 커넥션 정리 주기(초) */
|
||||
private static final int IDLE_CONNECTION_EVICT_SECONDS = 30;
|
||||
|
||||
/**
|
||||
* mTLS 여부/clientId 조합별로 CloseableHttpClient(및 내부 PoolingHttpClientConnectionManager)를
|
||||
* 1회만 생성해 재사용한다. 이 서비스 인스턴스는 HttpClientAccessTokenServiceFactoryByDB 에
|
||||
* className 기준으로 캐시되어 재사용되므로, 이 필드도 인스턴스 생명주기 동안 안전하게 재사용된다.
|
||||
* (어댑터그룹별 설정을 들고 있는 것이 아니므로 stateless 원칙에 어긋나지 않는다)
|
||||
*/
|
||||
private final ConcurrentHashMap<String, CloseableHttpClient> httpClientCache = new ConcurrentHashMap<String, CloseableHttpClient>();
|
||||
|
||||
/**
|
||||
* 1. 기능 : 토큰 발급에 사용 2. 처리 개요 : - 속성 정보를 설정 하고 토큰 발급 URL 호출 한다. 3. 주의사항
|
||||
*
|
||||
* @param adapterProp Http Adapter 속성 정보
|
||||
* @return 반환 된 AccessTokenVO
|
||||
* @exception Exception 수동 시스템 간 통신 중 발생
|
||||
**/
|
||||
public AccessTokenVO execute(String name, Properties adapterProp, OutboundOAuthCredentialVo oAuthCredentialVo)
|
||||
throws Exception {
|
||||
AdapterGroupVO gvo = AdapterManager.getInstance().getAdapterGroupVO(name);
|
||||
String adapterUrl = adapterProp.getProperty("URL");
|
||||
String encode = gvo.getMessageEncode();
|
||||
String timeoutTemp = adapterProp.getProperty("HTTP_TIME_OUT");
|
||||
if (StringUtils.isBlank(timeoutTemp)) {
|
||||
timeoutTemp = "30000";
|
||||
}
|
||||
String connectionTimeoutTemp = adapterProp.getProperty("CONNECTION_TIMEOUT");
|
||||
if (StringUtils.isBlank(connectionTimeoutTemp)) {
|
||||
connectionTimeoutTemp = "30000";
|
||||
}
|
||||
|
||||
int timeout = Integer.parseInt(timeoutTemp);
|
||||
int connectionTimeout = Integer.parseInt(connectionTimeoutTemp);
|
||||
long currentTime = System.currentTimeMillis();
|
||||
|
||||
String uri = oAuthCredentialVo.getUrl();
|
||||
|
||||
if (!UrlUtils.isAbsoluteUrl(uri)) {
|
||||
uri = appendPath(adapterUrl, uri);
|
||||
}
|
||||
|
||||
// content-type 은 프로퍼티 > VO > 기본값 순으로 결정한다.
|
||||
String contentType = getProp(name, "content-type", null);
|
||||
if (StringUtils.isBlank(contentType)) {
|
||||
contentType = oAuthCredentialVo.getContentType();
|
||||
}
|
||||
if (StringUtils.isBlank(contentType)) {
|
||||
contentType = CONTENT_TYPE_FORM;
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("Content-Type not specified. Using default: " + CONTENT_TYPE_FORM);
|
||||
}
|
||||
}
|
||||
|
||||
String method = getProp(name, "method", "POST");
|
||||
|
||||
Charset charset;
|
||||
if (StringUtils.isNotBlank(encode)) {
|
||||
charset = Charset.forName(encode);
|
||||
} else {
|
||||
charset = Charset.defaultCharset();
|
||||
encode = charset.toString();
|
||||
}
|
||||
|
||||
boolean useForwardProxy = StringUtils.equalsIgnoreCase(adapterProp.getProperty("FORWARD_PROXY_USE_YN"), "Y");
|
||||
String forwardProxyUrl = adapterProp.getProperty("FORWARD_PROXY_URL");
|
||||
|
||||
if (logger.isDebug()) {
|
||||
logger.debug(
|
||||
"contentType:{}, method:{}, uri:{}, charset:{}, transactionTimeout:{}, connectionTimeout:{}, useForwardProxy:{}, forwardProxyUrl:{}",
|
||||
contentType, method, uri, encode, timeout, connectionTimeout, useForwardProxy, forwardProxyUrl);
|
||||
}
|
||||
|
||||
// mTLS config with default connection parameters
|
||||
boolean useMtls = StringUtils.equalsIgnoreCase(adapterProp.getProperty(HttpClientAdapterServiceKey.USE_MTLS),
|
||||
"Y");
|
||||
AdapterGroupVO adapterGroup = AdapterManager.getInstance().getAdapterGroup(name);
|
||||
String clientId = adapterGroup.getClientId();
|
||||
|
||||
if (logger.isInfo()) {
|
||||
logger.info("MTLS adapterGroupName. : {}, useMtls. : {}, clientId : {}", name, useMtls, clientId);
|
||||
}
|
||||
|
||||
// mTLS 여부(및 clientId)별로 CloseableHttpClient 를 재사용한다. 매 호출마다 새로 만들면
|
||||
// PoolingHttpClientConnectionManager 를 쓰는 의미가 없어지고 SSL 핸드셰이크 비용만 반복된다.
|
||||
boolean contentCompression = !"N".equalsIgnoreCase(getProp(name, "http.content-compression", "Y"));
|
||||
boolean defaultUserAgent = !"N".equalsIgnoreCase(getProp(name, "http.default-user-agent", "Y"));
|
||||
// HttpClient 옵션도 캐시 키에 포함해야 어댑터그룹별 설정이 서로 섞이지 않는다.
|
||||
String httpClientCacheKey = (useMtls ? "mtls:" + clientId : "default") + "|zip=" + contentCompression + "|ua="
|
||||
+ defaultUserAgent;
|
||||
CloseableHttpClient httpClient = httpClientCache.computeIfAbsent(httpClientCacheKey,
|
||||
key -> buildHttpClient(useMtls, clientId, name, contentCompression, defaultUserAgent));
|
||||
|
||||
// 자격증명 전달 방식과 필드명 매핑 (모두 지역변수 — 이 클래스는 캐싱되는 싱글턴이므로 설정 상태를 갖지 않는다)
|
||||
String location = getProp(name, "credential.location", LOCATION_BODY);
|
||||
if (!LOCATION_BODY.equalsIgnoreCase(location) && !LOCATION_BASIC_HEADER.equalsIgnoreCase(location)
|
||||
&& !LOCATION_CUSTOM_HEADER.equalsIgnoreCase(location)) {
|
||||
throw new Exception("Unsupported credential.location. property [" + name + ".credential.location] = "
|
||||
+ location);
|
||||
}
|
||||
String fieldClientId = getFieldName(name, "request.field.client-id", "client_id");
|
||||
String fieldClientSecret = getFieldName(name, "request.field.client-secret", "client_secret");
|
||||
String fieldGrantType = getFieldName(name, "request.field.grant-type", "grant_type");
|
||||
String fieldScope = getFieldName(name, "request.field.scope", "scope");
|
||||
|
||||
// request.fields.in-header 에 나열한 필드는 바디가 아니라 헤더로 보낸다.
|
||||
Set<String> inHeaderFields = parseInHeaderFields(name);
|
||||
|
||||
warnIgnoredCredentialFields(name, location, inHeaderFields);
|
||||
|
||||
// 요청 바디(또는 GET 쿼리) 파라미터와 요청 헤더 구성
|
||||
Map<String, String> params = new LinkedHashMap<String, String>();
|
||||
Map<String, String> headers = new LinkedHashMap<String, String>();
|
||||
|
||||
if (LOCATION_BODY.equalsIgnoreCase(location)) {
|
||||
assignField(params, headers, inHeaderFields, FIELD_CLIENT_ID, fieldClientId,
|
||||
oAuthCredentialVo.getClientId());
|
||||
assignField(params, headers, inHeaderFields, FIELD_CLIENT_SECRET, fieldClientSecret,
|
||||
oAuthCredentialVo.getClientSecret());
|
||||
}
|
||||
assignField(params, headers, inHeaderFields, FIELD_SCOPE, fieldScope, oAuthCredentialVo.getScope());
|
||||
assignField(params, headers, inHeaderFields, FIELD_GRANT_TYPE, fieldGrantType,
|
||||
oAuthCredentialVo.getGrantType());
|
||||
|
||||
// 관리화면에서 입력한 bodyJson (기존 구현체와의 호환)
|
||||
String addBodyJson = oAuthCredentialVo.getBodyJson();
|
||||
if (StringUtils.isNotBlank(addBodyJson)) {
|
||||
ObjectMapper bodyJsonMapper = JacksonUtil.newNumberSafeMapper();
|
||||
Map<String, Object> addBodyMap = bodyJsonMapper.readValue(addBodyJson, Map.class);
|
||||
for (Map.Entry<String, Object> entry : addBodyMap.entrySet()) {
|
||||
if (entry.getValue() != null) {
|
||||
params.put(entry.getKey(), entry.getValue().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
// {어댑터그룹명}.body.* 로 설정한 고정 파라미터
|
||||
params.putAll(getPropsByPrefix(name, "body."));
|
||||
|
||||
// 관리화면에서 입력한 headerJson (기존 구현체와의 호환)
|
||||
String addHeaderJson = oAuthCredentialVo.getHeaderJson();
|
||||
if (StringUtils.isNotBlank(addHeaderJson)) {
|
||||
ObjectMapper headerJsonMapper = JacksonUtil.newNumberSafeMapper();
|
||||
Map<String, Object> addHeaderMap = headerJsonMapper.readValue(addHeaderJson, Map.class);
|
||||
for (Map.Entry<String, Object> entry : addHeaderMap.entrySet()) {
|
||||
if (entry.getValue() != null) {
|
||||
headers.put(entry.getKey(), entry.getValue().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
// {어댑터그룹명}.header.* 로 설정한 고정 헤더
|
||||
headers.putAll(getPropsByPrefix(name, "header."));
|
||||
|
||||
if (!LOCATION_BODY.equalsIgnoreCase(location)) {
|
||||
boolean basic = LOCATION_BASIC_HEADER.equalsIgnoreCase(location);
|
||||
|
||||
// basic-header 는 Authorization: Basic base64(id:secret) 가 기본,
|
||||
// custom-header 는 헤더명/접두어/값 출처를 모두 설정으로 정한다.
|
||||
String headerName = getProp(name, "credential.header.name", basic ? "Authorization" : null);
|
||||
if (StringUtils.isBlank(headerName)) {
|
||||
throw new Exception("credential.location=" + location + " requires " + PROP_GROUP + " property ["
|
||||
+ name + ".credential.header.name]");
|
||||
}
|
||||
String prefix = getProp(name, "credential.header.prefix", basic ? "Basic" : null);
|
||||
String source = getProp(name, "credential.header.value", basic ? SOURCE_BASIC : SOURCE_CLIENT_SECRET);
|
||||
|
||||
headers.put(headerName, join(prefix, resolveCredentialValue(name, source, oAuthCredentialVo)));
|
||||
|
||||
String idHeaderName = getProp(name, "credential.header.client-id.name", null);
|
||||
if (StringUtils.isNotBlank(idHeaderName)) {
|
||||
headers.put(idHeaderName, oAuthCredentialVo.getClientId());
|
||||
}
|
||||
}
|
||||
|
||||
// httpClient 는 캐시해서 재사용하므로 close 하지 않는다. (닫으면 커넥션 풀이 함께 종료된다)
|
||||
{
|
||||
HttpUriRequestBase request;
|
||||
|
||||
if ("GET".equalsIgnoreCase(method)) {
|
||||
URIBuilder uriBuilder = new URIBuilder(uri, charset);
|
||||
for (Map.Entry<String, String> entry : params.entrySet()) {
|
||||
uriBuilder.addParameter(entry.getKey(), entry.getValue());
|
||||
}
|
||||
request = new HttpGet(uriBuilder.build());
|
||||
} else {
|
||||
request = new HttpPost(uri);
|
||||
request.setHeader("Content-Type", buildContentTypeHeader(name, contentType, encode));
|
||||
|
||||
if (params.isEmpty()) {
|
||||
// 전송할 파라미터가 없으면 바디를 붙이지 않는다. (헤더로만 인증하는 기관 대응)
|
||||
if (logger.isDebug()) {
|
||||
logger.debug("No request parameter. Sending no body. adapterGroupName : " + name);
|
||||
}
|
||||
} else if (isJsonContentType(contentType)) {
|
||||
ObjectMapper objMapper = JacksonUtil.newNumberSafeMapper();
|
||||
ObjectNode bodyNode = objMapper.createObjectNode();
|
||||
for (Map.Entry<String, String> entry : params.entrySet()) {
|
||||
putJsonPath(bodyNode, entry.getKey(), entry.getValue());
|
||||
}
|
||||
request.setEntity(new StringEntity(objMapper.writeValueAsString(bodyNode), charset));
|
||||
} else if (isFormContentType(contentType)) {
|
||||
List<NameValuePair> formParams = new ArrayList<NameValuePair>();
|
||||
for (Map.Entry<String, String> entry : params.entrySet()) {
|
||||
formParams.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
|
||||
}
|
||||
request.setEntity(new UrlEncodedFormEntity(formParams, charset));
|
||||
} else {
|
||||
// 조용히 form 으로 내보내면 상대 서버가 400 을 줬을 때 원인을 찾기 어렵다.
|
||||
throw new Exception("Unsupported content-type for token request. property [" + name
|
||||
+ ".content-type] = " + contentType);
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
request.setHeader(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
|
||||
|
||||
if (useForwardProxy) {
|
||||
java.net.URL url = new java.net.URL(forwardProxyUrl);
|
||||
|
||||
// 프로토콜, 호스트, 포트 추출
|
||||
String protocol = url.getProtocol();
|
||||
String host = url.getHost();
|
||||
int port = url.getPort();
|
||||
HttpHost proxy = new HttpHost(protocol, host, port);
|
||||
requestConfigBuilder.setProxy(proxy);
|
||||
}
|
||||
|
||||
// 커넥션 풀 대기 시간을 지정하지 않으면 기본 3분을 기다린다. 스케줄러의 태스크 타임아웃(30초)이
|
||||
// 먼저 걸려 요청이 인터럽트되고, 그 과정에서 커넥션이 반납되지 않아 풀이 마르는 악순환이 생긴다.
|
||||
RequestConfig requestConfig = requestConfigBuilder
|
||||
.setConnectTimeout(Timeout.ofMilliseconds(connectionTimeout))
|
||||
.setConnectionRequestTimeout(Timeout.ofMilliseconds(connectionTimeout))
|
||||
.setResponseTimeout(Timeout.ofMilliseconds(timeout)).build();
|
||||
request.setConfig(requestConfig);
|
||||
|
||||
if (logger.isDebug()) {
|
||||
// client_secret 은 로그에 남기지 않는다.
|
||||
logger.debug("uri = [" + uri + "]");
|
||||
logger.debug("method = [" + method + "]");
|
||||
logger.debug("credentialLoc = [" + location + "]");
|
||||
logger.debug("oauthClientId = [" + oAuthCredentialVo.getClientId() + "]");
|
||||
logger.debug("oauthScope = [" + oAuthCredentialVo.getScope() + "]");
|
||||
logger.debug("oauthGrantType = [" + oAuthCredentialVo.getGrantType() + "]");
|
||||
logger.debug("contentType = [" + contentType + "]");
|
||||
logger.debug("encode = [" + encode + "]");
|
||||
logger.debug("requestParamNames = " + params.keySet());
|
||||
logger.debug("requestHeaderNames= " + headers.keySet());
|
||||
}
|
||||
|
||||
try (CloseableHttpResponse response = httpClient.execute(request)) {
|
||||
if (response.getCode() / 100 != 2) {
|
||||
throw new Exception("OAuth token receive status fail value= " + response.getCode());
|
||||
}
|
||||
|
||||
HttpEntity entity = response.getEntity();
|
||||
String responseString = EntityUtils.toString(entity, encode);
|
||||
logger.debug("oauthToken RECV = [" + responseString + "]");
|
||||
|
||||
if (StringUtils.isBlank(responseString)) {
|
||||
throw new Exception("oauth token return null");
|
||||
}
|
||||
|
||||
return parseToken(name, responseString, currentTime, oAuthCredentialVo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 응답 전문을 프로퍼티에 설정된 필드명 매핑에 따라 AccessTokenVO 로 변환한다.
|
||||
*
|
||||
* @param name 어댑터그룹명
|
||||
* @param responseString 응답 전문
|
||||
* @param currentTime 요청 시각 (만료시각 계산 기준)
|
||||
* @param vo oauth 인증 정보. 응답에 expires_in 이 없을 때 intervalSec 을 사용한다.
|
||||
* @return AccessTokenVO
|
||||
* @throws Exception 성공코드 불일치, access_token 미존재 시
|
||||
*/
|
||||
AccessTokenVO parseToken(String name, String responseString, long currentTime,
|
||||
OutboundOAuthCredentialVo vo) throws Exception {
|
||||
int intervalSec = vo.getIntervalSec();
|
||||
ObjectMapper objectMapper = JacksonUtil.newNumberSafeMapper();
|
||||
JsonNode root = objectMapper.readTree(responseString);
|
||||
|
||||
// 응답 성공여부 판정 (설정한 경우에만)
|
||||
String successField = getProp(name, "response.success.field", null);
|
||||
if (StringUtils.isNotBlank(successField)) {
|
||||
String expected = getProp(name, "response.success.value", null);
|
||||
JsonNode successNode = findByPath(root, successField);
|
||||
String actual = (successNode == null) ? null : successNode.asText();
|
||||
if (successNode == null || (StringUtils.isNotBlank(expected) && !StringUtils.equals(expected, actual))) {
|
||||
throw new Exception("OAuth token response result code mismatch. field=" + successField + ", expected="
|
||||
+ expected + ", actual=" + actual);
|
||||
}
|
||||
}
|
||||
|
||||
OAuth2AccessTokenVO accessToken = new OAuth2AccessTokenVO();
|
||||
|
||||
// access_token 만 필수. 나머지는 없으면 없는 대로 진행한다.
|
||||
String tokenPath = getFieldName(name, "response.field.access-token", "access_token");
|
||||
JsonNode tokenNode = findByPath(root, tokenPath);
|
||||
if (tokenNode == null || StringUtils.isBlank(tokenNode.asText())) {
|
||||
throw new Exception("OAuth token not found in response. field=" + tokenPath);
|
||||
}
|
||||
accessToken.setAccessToken(tokenNode.asText());
|
||||
accessToken.setClientId(vo.getClientId());
|
||||
|
||||
JsonNode tokenTypeNode = findByPath(root, getFieldName(name, "response.field.token-type", "token_type"));
|
||||
if (tokenTypeNode != null && StringUtils.isNotBlank(tokenTypeNode.asText())) {
|
||||
accessToken.setTokenType(tokenTypeNode.asText());
|
||||
}
|
||||
|
||||
String expiresInPath = getFieldName(name, "response.field.expires-in", "expires_in");
|
||||
JsonNode expiresInNode = findByPath(root, expiresInPath);
|
||||
long expiresIn;
|
||||
if (expiresInNode != null && StringUtils.isNotBlank(expiresInNode.asText())) {
|
||||
expiresIn = expiresInNode.asLong();
|
||||
} else {
|
||||
// 응답에 만료시간이 없는 기관이 있다. 설정값 > 토큰 재발급 주기(intervalSec) > 1시간 순으로 사용한다.
|
||||
String configured = getProp(name, "response.expires-in.default", null);
|
||||
if (StringUtils.isNotBlank(configured)) {
|
||||
expiresIn = Long.parseLong(configured);
|
||||
} else if (intervalSec > 0) {
|
||||
expiresIn = intervalSec;
|
||||
} else {
|
||||
expiresIn = DEFAULT_EXPIRES_IN;
|
||||
}
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("OAuth token response has no [{}]. Using expires-in {} sec. adapterGroupName : {}",
|
||||
expiresInPath, expiresIn, name);
|
||||
}
|
||||
}
|
||||
accessToken.setExpiration(new Date(currentTime + expiresIn * 1000L));
|
||||
|
||||
JsonNode scopeNode = findByPath(root, getFieldName(name, "response.field.scope", "scope"));
|
||||
if (scopeNode != null && StringUtils.isNotBlank(scopeNode.asText())) {
|
||||
accessToken.setScope(scopeNode.asText());
|
||||
}
|
||||
|
||||
JsonNode clientUseCodeNode = findByPath(root,
|
||||
getFieldName(name, "response.field.client-use-code", "client_use_code"));
|
||||
if (clientUseCodeNode != null && StringUtils.isNotBlank(clientUseCodeNode.asText())) {
|
||||
accessToken.setClientUseCode(clientUseCodeNode.asText());
|
||||
}
|
||||
|
||||
logger.debug("oauthToken =" + accessToken.toString());
|
||||
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* credential.location 에 따라 무시되는 프로퍼티가 설정돼 있으면 경고를 남긴다.
|
||||
* (execute() 마다 호출되므로 토큰 갱신 주기마다 반복 출력된다 — 오설정을 놓치지 않기 위한 의도)
|
||||
*
|
||||
* @param name 어댑터그룹명
|
||||
* @param location credential.location 설정값
|
||||
* @param inHeaderFields request.fields.in-header 목록
|
||||
*/
|
||||
private void warnIgnoredCredentialFields(String name, String location, Set<String> inHeaderFields) {
|
||||
if (LOCATION_BODY.equalsIgnoreCase(location)) {
|
||||
return;
|
||||
}
|
||||
if (!logger.isWarn()) {
|
||||
return;
|
||||
}
|
||||
if (inHeaderFields.contains(FIELD_CLIENT_ID) || inHeaderFields.contains(FIELD_CLIENT_SECRET)) {
|
||||
logger.warn(
|
||||
"[{}] credential.location={} : client-id/client-secret in request.fields.in-header are ignored. adapterGroupName : {}",
|
||||
PROP_GROUP, location, name);
|
||||
}
|
||||
// 기본값이 아니라 실제 설정 여부를 봐야 하므로 default 없는 조회를 쓴다. (none 은 끈 것이므로 제외)
|
||||
if (isExplicitFieldName(getProp(name, "request.field.client-id", null))
|
||||
|| isExplicitFieldName(getProp(name, "request.field.client-secret", null))) {
|
||||
logger.warn(
|
||||
"[{}] credential.location={} : request.field.client-id/client-secret are ignored. adapterGroupName : {}",
|
||||
PROP_GROUP, location, name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Content-Type 헤더값을 만든다.
|
||||
* charset 이 이미 들어있거나 "{어댑터그룹명}.content-type.charset" 이 N 이면 charset 을 붙이지 않는다.
|
||||
* (특히 application/x-www-form-urlencoded 에 charset 이 붙으면 거부하는 기관이 있다)
|
||||
*
|
||||
* @param name 어댑터그룹명
|
||||
* @param contentType 결정된 content-type
|
||||
* @param encode 전문 인코딩
|
||||
* @return Content-Type 헤더값
|
||||
*/
|
||||
String buildContentTypeHeader(String name, String contentType, String encode) {
|
||||
String value = StringUtils.trim(contentType);
|
||||
if (StringUtils.containsIgnoreCase(value, "charset")) {
|
||||
return value;
|
||||
}
|
||||
if (!"Y".equalsIgnoreCase(getProp(name, "content-type.charset", "Y"))) {
|
||||
return StringUtils.removeEnd(value, ";");
|
||||
}
|
||||
// VO 에 "application/json;" 처럼 세미콜론까지 들어있는 경우가 있어 구분자 중복을 막는다.
|
||||
if (StringUtils.endsWith(value, ";")) {
|
||||
return value + " charset=" + encode;
|
||||
}
|
||||
return value + "; charset=" + encode;
|
||||
}
|
||||
|
||||
/**
|
||||
* mTLS 여부/clientId 조합에 맞는 SSLContext 로 PoolingHttpClientConnectionManager 와
|
||||
* CloseableHttpClient 를 생성한다. httpClientCache 에 의해 조합당 1회만 호출되며, 반환된
|
||||
* CloseableHttpClient 는 재사용을 위해 닫지 않는다(닫으면 커넥션 풀이 함께 종료된다).
|
||||
*
|
||||
* @param useMtls mTLS 사용 여부
|
||||
* @param clientId mTLS 인증서 조회용 clientId
|
||||
* @param adapterGroupName 어댑터그룹명 (로그용)
|
||||
* @param contentCompression false 이면 Accept-Encoding: gzip 을 붙이지 않는다.
|
||||
* @param defaultUserAgent false 이면 기본 User-Agent 를 보내지 않는다.
|
||||
* @return 재사용할 CloseableHttpClient
|
||||
*/
|
||||
private CloseableHttpClient buildHttpClient(boolean useMtls, String clientId, String adapterGroupName,
|
||||
boolean contentCompression, boolean defaultUserAgent) {
|
||||
int maxTotalConnections = HttpClientAdapterServiceKey.DEFAULT_MAX_TOTAL_CONNECTIONS;
|
||||
int maxHostConnections = HttpClientAdapterServiceKey.DEFAULT_MAX_CONNECTION_PER_HOST;
|
||||
|
||||
HttpOutTlsInfoVO mtlsInfo = null;
|
||||
SSLContext sslContext = null;
|
||||
PoolingHttpClientConnectionManagerBuilder cmBuilder = PoolingHttpClientConnectionManagerBuilder.create();
|
||||
|
||||
try {
|
||||
if (useMtls) {
|
||||
HttpOutTlsInfoManager tlsManager = HttpOutTlsInfoManager.getInstance();
|
||||
if (StringUtils.isNotEmpty(clientId)) {
|
||||
mtlsInfo = tlsManager.getHttpOutTlsInfo(clientId);
|
||||
}
|
||||
}
|
||||
|
||||
if (useMtls && mtlsInfo != null) {
|
||||
String storeType = mtlsInfo.getStoreType();
|
||||
String keyStoreInfo = mtlsInfo.getKeystoreInfo();
|
||||
String keyStorePassword = mtlsInfo.getKeystorePassword();
|
||||
String trustStoreInfo = mtlsInfo.getTruststoreInfo();
|
||||
String trustStorePassword = mtlsInfo.getTruststorePassword();
|
||||
|
||||
String[] tlsVersions = null;
|
||||
String[] cipherSuites = null;
|
||||
|
||||
if (StringUtils.isAnyEmpty(keyStoreInfo, keyStorePassword)) {
|
||||
throw new Exception("mTLS keyStore config error");
|
||||
}
|
||||
|
||||
boolean skipTrust = false;
|
||||
if (StringUtils.isAnyEmpty(trustStoreInfo, trustStorePassword)) {
|
||||
if (logger.isWarn())
|
||||
logger.warn("Skip trustStore validation adapterGroupName : " + adapterGroupName);
|
||||
skipTrust = true;
|
||||
}
|
||||
|
||||
sslContext = HttpClient5SSLContextFactory.createMTLSContextFromContent(storeType, keyStoreInfo,
|
||||
keyStorePassword, trustStoreInfo, trustStorePassword, skipTrust, tlsVersions, cipherSuites);
|
||||
|
||||
SSLConnectionSocketFactory sslSocketFactory = null;
|
||||
if (testMode) {
|
||||
sslSocketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
|
||||
} else {
|
||||
sslSocketFactory = new SSLConnectionSocketFactory(sslContext);
|
||||
}
|
||||
cmBuilder.setSSLSocketFactory(sslSocketFactory);
|
||||
} else {
|
||||
sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build();
|
||||
SSLConnectionSocketFactory csf = null;
|
||||
if (testMode) {
|
||||
csf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
|
||||
} else {
|
||||
csf = new SSLConnectionSocketFactory(sslContext);
|
||||
}
|
||||
cmBuilder.setSSLSocketFactory(csf);
|
||||
}
|
||||
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException | CertificateException
|
||||
| IOException | UnrecoverableKeyException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
PoolingHttpClientConnectionManager connectionManager = cmBuilder.build();
|
||||
connectionManager.setMaxTotal(maxTotalConnections);
|
||||
connectionManager.setDefaultMaxPerRoute(maxHostConnections);
|
||||
|
||||
if (logger.isInfo()) {
|
||||
logger.info(
|
||||
"HttpClientAccessTokenServiceWithConfig] HttpClient(재사용) 생성. adapterGroupName={}, useMtls={}, clientId={}, contentCompression={}, defaultUserAgent={}",
|
||||
adapterGroupName, useMtls, clientId, contentCompression, defaultUserAgent);
|
||||
}
|
||||
|
||||
// HttpClient 를 캐시해 재사용하므로 죽은 커넥션이 풀에 남지 않도록 정리 설정을 건다.
|
||||
HttpClientBuilder builder = HttpClients.custom().setConnectionManager(connectionManager)
|
||||
.evictExpiredConnections()
|
||||
.evictIdleConnections(TimeValue.ofSeconds(IDLE_CONNECTION_EVICT_SECONDS));
|
||||
if (!contentCompression) {
|
||||
builder.disableContentCompression();
|
||||
}
|
||||
if (!defaultUserAgent) {
|
||||
builder.disableDefaultUserAgent();
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* credential.header.value 설정에 따라 헤더에 담을 값을 만든다.
|
||||
*
|
||||
* @param name 어댑터그룹명
|
||||
* @param source basic | client-id | client-secret
|
||||
* @param vo oauth 인증 정보
|
||||
* @return 헤더에 담을 값
|
||||
* @throws Exception 지원하지 않는 설정값인 경우
|
||||
*/
|
||||
String resolveCredentialValue(String name, String source, OutboundOAuthCredentialVo vo) throws Exception {
|
||||
if (SOURCE_BASIC.equalsIgnoreCase(source)) {
|
||||
String authValue = vo.getClientId() + ":" + vo.getClientSecret();
|
||||
return Base64.getEncoder().encodeToString(authValue.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
if (SOURCE_CLIENT_ID.equalsIgnoreCase(source)) {
|
||||
// 이미 인코딩된 값을 clientId 에 저장해 두고 그대로 보내는 기관이 있다.
|
||||
return vo.getClientId();
|
||||
}
|
||||
if (SOURCE_CLIENT_SECRET.equalsIgnoreCase(source)) {
|
||||
return vo.getClientSecret();
|
||||
}
|
||||
throw new Exception("Unsupported credential.header.value. property [" + name + ".credential.header.value] = "
|
||||
+ source);
|
||||
}
|
||||
|
||||
/**
|
||||
* "{어댑터그룹명}.request.fields.in-header" 설정을 파싱한다.
|
||||
* 여기 나열한 필드는 바디가 아니라 헤더로 전송한다. (ex. "client-id,client-secret,scope,grant-type")
|
||||
*
|
||||
* @param name 어댑터그룹명
|
||||
* @return 헤더로 보낼 필드 이름 집합. 미설정이면 빈 집합.
|
||||
*/
|
||||
Set<String> parseInHeaderFields(String name) {
|
||||
Set<String> fields = new LinkedHashSet<String>();
|
||||
String configured = getProp(name, "request.fields.in-header", null);
|
||||
if (StringUtils.isBlank(configured)) {
|
||||
return fields;
|
||||
}
|
||||
for (String token : StringUtils.split(configured, ',')) {
|
||||
String field = StringUtils.lowerCase(StringUtils.trim(token));
|
||||
if (StringUtils.isNotBlank(field)) {
|
||||
fields.add(field);
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 필드 하나를 in-header 설정에 따라 헤더 또는 바디(GET 이면 쿼리)에 담는다.
|
||||
* 어느 쪽이든 이름은 "{어댑터그룹명}.request.field.*" 로 매핑한 값을 쓴다.
|
||||
*
|
||||
* @param params 바디/쿼리 파라미터
|
||||
* @param headers 요청 헤더
|
||||
* @param inHeaderFields 헤더로 보낼 필드 목록
|
||||
* @param field 필드 이름 (client-id / client-secret / grant-type / scope)
|
||||
* @param mappedName 매핑된 파라미터명 또는 헤더명. none 으로 끈 경우 null.
|
||||
* @param value 전송할 값
|
||||
*/
|
||||
void assignField(Map<String, String> params, Map<String, String> headers, Set<String> inHeaderFields, String field,
|
||||
String mappedName, String value) {
|
||||
if (inHeaderFields.contains(field)) {
|
||||
putIfNotBlank(headers, mappedName, value);
|
||||
} else {
|
||||
putIfNotBlank(params, mappedName, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 실제로 필드명이 설정돼 있는지 확인한다. 미설정이거나 none(끔) 이면 false.
|
||||
*
|
||||
* @param value 프로퍼티 설정값
|
||||
* @return 필드명이 설정돼 있으면 true
|
||||
*/
|
||||
private boolean isExplicitFieldName(String value) {
|
||||
return StringUtils.isNotBlank(value) && !FIELD_NONE.equalsIgnoreCase(StringUtils.trim(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* 필드명 매핑을 조회한다. 설정값이 none 이면 그 필드를 사용하지 않겠다는 의미이므로 null 을 반환한다.
|
||||
*
|
||||
* @param name 어댑터그룹명
|
||||
* @param key 프로퍼티 키
|
||||
* @param def 미설정 시 기본 필드명
|
||||
* @return 필드명. 사용하지 않으면 null.
|
||||
*/
|
||||
String getFieldName(String name, String key, String def) {
|
||||
String value = StringUtils.trim(getProp(name, key, def));
|
||||
if (FIELD_NONE.equalsIgnoreCase(value)) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* content-type 을 파라미터 부분을 뗀 소문자로 정규화한다. (ex. "Application/JSON; charset=UTF-8" → "application/json")
|
||||
*
|
||||
* @param contentType content-type 설정값
|
||||
* @return 정규화된 content-type
|
||||
*/
|
||||
private String normalizeContentType(String contentType) {
|
||||
return StringUtils.lowerCase(StringUtils.trim(StringUtils.substringBefore(contentType, ";")));
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 계열 content-type 인지 확인한다. (application/json, application/vnd.xxx+json 등)
|
||||
*
|
||||
* @param contentType content-type 설정값
|
||||
* @return JSON 이면 true
|
||||
*/
|
||||
boolean isJsonContentType(String contentType) {
|
||||
return StringUtils.contains(normalizeContentType(contentType), "json");
|
||||
}
|
||||
|
||||
/**
|
||||
* form-urlencoded content-type 인지 확인한다.
|
||||
*
|
||||
* @param contentType content-type 설정값
|
||||
* @return form-urlencoded 이면 true
|
||||
*/
|
||||
boolean isFormContentType(String contentType) {
|
||||
return StringUtils.contains(normalizeContentType(contentType), "x-www-form-urlencoded");
|
||||
}
|
||||
|
||||
/**
|
||||
* PropManager 에서 "{어댑터그룹명}.{key}" 프로퍼티를 조회한다.
|
||||
*
|
||||
* @param name 어댑터그룹명
|
||||
* @param key 프로퍼티 키 (어댑터그룹명 이후 부분)
|
||||
* @param def 미설정 시 반환할 기본값
|
||||
* @return 프로퍼티 값
|
||||
*/
|
||||
private String getProp(String name, String key, String def) {
|
||||
return PropManager.getInstance().getProperty(PROP_GROUP, name + "." + key, def);
|
||||
}
|
||||
|
||||
/**
|
||||
* "{어댑터그룹명}.{prefix}" 로 시작하는 프로퍼티를 모두 조회한다. (header./body. 다건 설정용)
|
||||
*
|
||||
* @param name 어댑터그룹명
|
||||
* @param prefix 조회할 접두어. 마침표까지 포함한다. (ex. "header.")
|
||||
* @return 접두어를 제거한 키와 값의 Map. 설정이 없으면 빈 Map.
|
||||
*/
|
||||
Map<String, String> getPropsByPrefix(String name, String prefix) {
|
||||
Map<String, String> result = new LinkedHashMap<String, String>();
|
||||
PropManager propManager = PropManager.getInstance();
|
||||
// getProperties() 는 그룹이 없으면 RuntimeException 이므로 먼저 확인한다.
|
||||
if (!propManager.isContainProperties(PROP_GROUP)) {
|
||||
return result;
|
||||
}
|
||||
String full = name + "." + prefix;
|
||||
Properties properties = propManager.getProperties(PROP_GROUP);
|
||||
for (String key : properties.stringPropertyNames()) {
|
||||
if (key.startsWith(full) && key.length() > full.length()) {
|
||||
result.put(key.substring(full.length()), properties.getProperty(key));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* dot-path 로 JsonNode 를 탐색한다. (ex. "dataBody.access_token")
|
||||
*
|
||||
* @param root 응답 JSON 루트
|
||||
* @param path 조회 경로
|
||||
* @return 찾은 노드. 경로가 없거나 null 노드면 null.
|
||||
*/
|
||||
JsonNode findByPath(JsonNode root, String path) {
|
||||
if (root == null || StringUtils.isBlank(path)) {
|
||||
return null;
|
||||
}
|
||||
JsonNode node = root;
|
||||
for (String token : StringUtils.split(path, '.')) {
|
||||
if (node == null) {
|
||||
return null;
|
||||
}
|
||||
node = node.get(token);
|
||||
}
|
||||
if (node == null || node.isNull()) {
|
||||
return null;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* dot-path 로 JSON 요청 바디에 값을 넣는다. 경로 중간 노드는 없으면 생성한다. (ex. "auth.clientId")
|
||||
*
|
||||
* @param root 요청 바디 루트
|
||||
* @param path 필드 경로
|
||||
* @param value 설정할 값
|
||||
*/
|
||||
void putJsonPath(ObjectNode root, String path, String value) {
|
||||
String[] tokens = StringUtils.split(path, '.');
|
||||
ObjectNode node = root;
|
||||
for (int i = 0; i < tokens.length - 1; i++) {
|
||||
JsonNode child = node.get(tokens[i]);
|
||||
if (child instanceof ObjectNode) {
|
||||
node = (ObjectNode) child;
|
||||
} else {
|
||||
node = node.putObject(tokens[i]);
|
||||
}
|
||||
}
|
||||
node.put(tokens[tokens.length - 1], value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 헤더값 접두어와 값을 공백으로 잇는다. 접두어가 없으면 값만 반환한다.
|
||||
*
|
||||
* @param prefix 접두어 (ex. "Basic")
|
||||
* @param value 값
|
||||
* @return 완성된 헤더값
|
||||
*/
|
||||
private String join(String prefix, String value) {
|
||||
if (StringUtils.isBlank(prefix)) {
|
||||
return value;
|
||||
}
|
||||
return prefix + " " + value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 값이 비어있지 않을 때만 Map 에 담는다.
|
||||
*
|
||||
* @param params 대상 Map
|
||||
* @param key 파라미터명
|
||||
* @param value 파라미터값
|
||||
*/
|
||||
private void putIfNotBlank(Map<String, String> params, String key, String value) {
|
||||
if (StringUtils.isNotBlank(key) && StringUtils.isNotBlank(value)) {
|
||||
params.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* URL에 경로를 추가합니다. 중복되는 슬래시를 방지합니다.
|
||||
*
|
||||
* @param baseUrl 기본 URL
|
||||
* @param pathToAdd 추가할 경로
|
||||
* @return 완성된 URL 문자열
|
||||
*/
|
||||
public static String appendPath(String baseUrl, String pathToAdd) {
|
||||
if (!baseUrl.endsWith("/") && !pathToAdd.startsWith("/")) {
|
||||
return baseUrl + "/" + pathToAdd;
|
||||
} else if (baseUrl.endsWith("/") && pathToAdd.startsWith("/")) {
|
||||
return baseUrl + pathToAdd.substring(1);
|
||||
} else {
|
||||
return baseUrl + pathToAdd;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-6
@@ -180,12 +180,7 @@ public class HttpClientAccessTokenServiceWithDefault implements HttpClientAccess
|
||||
cmBuilder.setSSLSocketFactory(sslSocketFactory);
|
||||
} else {
|
||||
sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build();
|
||||
SSLConnectionSocketFactory csf = null;
|
||||
if (testMode) {
|
||||
csf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
|
||||
} else {
|
||||
csf = new SSLConnectionSocketFactory(sslContext);
|
||||
}
|
||||
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext);
|
||||
cmBuilder.setSSLSocketFactory(csf);
|
||||
}
|
||||
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException | CertificateException
|
||||
|
||||
+1
-6
@@ -179,12 +179,7 @@ public class HttpClientAccessTokenServiceWithParam implements HttpClientAccessTo
|
||||
cmBuilder.setSSLSocketFactory(sslSocketFactory);
|
||||
} else {
|
||||
sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build();
|
||||
SSLConnectionSocketFactory csf = null;
|
||||
if (testMode) {
|
||||
csf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
|
||||
} else {
|
||||
csf = new SSLConnectionSocketFactory(sslContext);
|
||||
}
|
||||
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext);
|
||||
cmBuilder.setSSLSocketFactory(csf);
|
||||
}
|
||||
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException | CertificateException
|
||||
|
||||
+1
-6
@@ -196,12 +196,7 @@ public class HttpClientAccessTokenServiceWithParamAddBody implements HttpClientA
|
||||
cmBuilder.setSSLSocketFactory(sslSocketFactory);
|
||||
} else {
|
||||
sslContext = SSLContextBuilder.create().loadTrustMaterial(new TrustAllStrategy()).build();
|
||||
SSLConnectionSocketFactory csf = null;
|
||||
if (testMode) {
|
||||
csf = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
|
||||
} else {
|
||||
csf = new SSLConnectionSocketFactory(sslContext);
|
||||
}
|
||||
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext);
|
||||
cmBuilder.setSSLSocketFactory(csf);
|
||||
}
|
||||
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException | CertificateException
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
package com.eactive.eai.authserver.jwt;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.security.oauth2.common.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.provider.OAuth2Authentication;
|
||||
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* PS256(RSA-PSS) JWT를 발급·검증하는 JwtAccessTokenConverter 확장 클래스.
|
||||
*
|
||||
* spring-security-jwt 1.1.1은 JwtAlgorithms에 PS256을 등록하지 않아
|
||||
* JwtHelper.encode/decodeAndVerify가 PS256 헤더를 처리하지 못한다.
|
||||
* 따라서 encode/decode를 직접 구현하여 JwtHelper를 완전히 우회한다.
|
||||
*/
|
||||
public class PssJwtAccessTokenConverter extends JwtAccessTokenConverter {
|
||||
|
||||
/** PS256 JWT 헤더 {"alg":"PS256","typ":"JWT"} 의 Base64URL 인코딩 (고정값) */
|
||||
private static final String ENCODED_HEADER = Base64.getUrlEncoder().withoutPadding()
|
||||
.encodeToString("{\"alg\":\"PS256\",\"typ\":\"JWT\"}".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
private final RsaPssSigner pssSigner;
|
||||
private final RsaPssVerifier pssVerifier;
|
||||
private final String publicKeyPem;
|
||||
private final ObjectMapper jackson = new ObjectMapper();
|
||||
|
||||
public PssJwtAccessTokenConverter(RSAPrivateKey privateKey, RSAPublicKey publicKey) {
|
||||
this.pssSigner = new RsaPssSigner(privateKey);
|
||||
this.pssVerifier = new RsaPssVerifier(publicKey);
|
||||
this.publicKeyPem = "-----BEGIN PUBLIC KEY-----\n"
|
||||
+ Base64.getMimeEncoder(64, new byte[]{'\n'}).encodeToString(publicKey.getEncoded())
|
||||
+ "\n-----END PUBLIC KEY-----";
|
||||
// 부모 setVerifierKey — afterPropertiesSet() 내부에서 참조하는 verifierKey 필드 초기화
|
||||
setVerifierKey(this.publicKeyPem);
|
||||
// getKey() (/oauth/token_key 엔드포인트)가 동작하도록 부모의 verifier 필드를 주입
|
||||
try {
|
||||
Field f = JwtAccessTokenConverter.class.getDeclaredField("verifier");
|
||||
f.setAccessible(true);
|
||||
f.set(this, pssVerifier);
|
||||
} catch (Exception ignored) { }
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
// 부모 afterPropertiesSet()은 signingKey 기반 초기화(JwtHelper 의존)를 시도하므로 건너뜀
|
||||
}
|
||||
|
||||
/**
|
||||
* PS256 JWT 발급. JwtHelper.encode를 사용하지 않고 직접 구성한다.
|
||||
* signingInput = Base64URL(header) + "." + Base64URL(payload)
|
||||
* JWT = signingInput + "." + Base64URL(PSS_sign(signingInput))
|
||||
*/
|
||||
@Override
|
||||
protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
|
||||
Map<String, ?> claims = getAccessTokenConverter().convertAccessToken(accessToken, authentication);
|
||||
String payload;
|
||||
try {
|
||||
payload = jackson.writeValueAsString(claims);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Access token을 JSON으로 변환 실패", e);
|
||||
}
|
||||
return buildJwt(payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* PS256 JWT 검증 및 클레임 추출. JwtHelper.decodeAndVerify를 사용하지 않고 직접 파싱한다.
|
||||
*/
|
||||
@Override
|
||||
protected Map<String, Object> decode(String token) {
|
||||
String[] parts = token.split("\\.");
|
||||
if (parts.length != 3) {
|
||||
throw new IllegalArgumentException("잘못된 JWT 형식");
|
||||
}
|
||||
byte[] signingInput = (parts[0] + "." + parts[1]).getBytes(StandardCharsets.US_ASCII);
|
||||
byte[] sig = Base64.getUrlDecoder().decode(padBase64Url(parts[2]));
|
||||
pssVerifier.verify(signingInput, sig); // 실패 시 InvalidSignatureException
|
||||
|
||||
byte[] payloadBytes = Base64.getUrlDecoder().decode(padBase64Url(parts[1]));
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> result = jackson.readValue(payloadBytes, Map.class);
|
||||
// JwtAccessTokenConverter 원본 동작 유지: exp 필드 Integer → Long 변환
|
||||
if (result.containsKey(EXP) && result.get(EXP) instanceof Integer) {
|
||||
result.put(EXP, ((Integer) result.get(EXP)).longValue());
|
||||
}
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("JWT 페이로드 파싱 실패", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** /oauth/token_key 엔드포인트 응답 — alg와 PEM 공개키 반환 */
|
||||
@Override
|
||||
public Map<String, String> getKey() {
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
result.put("alg", "PS256");
|
||||
result.put("value", publicKeyPem);
|
||||
return result;
|
||||
}
|
||||
|
||||
public String buildJwt(String payload) {
|
||||
String encodedPayload = Base64.getUrlEncoder().withoutPadding()
|
||||
.encodeToString(payload.getBytes(StandardCharsets.UTF_8));
|
||||
String signingInput = ENCODED_HEADER + "." + encodedPayload;
|
||||
byte[] sig = pssSigner.sign(signingInput.getBytes(StandardCharsets.US_ASCII));
|
||||
return signingInput + "." + Base64.getUrlEncoder().withoutPadding().encodeToString(sig);
|
||||
}
|
||||
|
||||
private static String padBase64Url(String s) {
|
||||
int mod = s.length() % 4;
|
||||
if (mod == 2) return s + "==";
|
||||
if (mod == 3) return s + "=";
|
||||
return s;
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package com.eactive.eai.authserver.jwt;
|
||||
|
||||
import java.security.Signature;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.spec.MGF1ParameterSpec;
|
||||
import java.security.spec.PSSParameterSpec;
|
||||
|
||||
import org.springframework.security.jwt.crypto.sign.Signer;
|
||||
|
||||
/**
|
||||
* PS256 (RSA-PSS + SHA-256) 서명 구현체.
|
||||
* RFC 7518 §3.5 파라미터: Hash=SHA-256, MGF=MGF1/SHA-256, saltLen=32, trailer=0xBC
|
||||
* JDK 11+ 내장 RSASSA-PSS 사용. JDK 8 환경은 BouncyCastle 프로바이더 등록이 필요하다.
|
||||
*
|
||||
* JwtHelper를 우회하는 PssJwtAccessTokenConverter와 함께 사용한다.
|
||||
* (spring-security-jwt 1.1.1은 JwtAlgorithms에 PS256을 등록하지 않아 JwtHelper 직접 사용 불가)
|
||||
*/
|
||||
public class RsaPssSigner implements Signer {
|
||||
|
||||
private static final PSSParameterSpec PSS_SPEC =
|
||||
new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1);
|
||||
|
||||
private final RSAPrivateKey privateKey;
|
||||
|
||||
public RsaPssSigner(RSAPrivateKey privateKey) {
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] sign(byte[] bytes) {
|
||||
try {
|
||||
Signature sig = Signature.getInstance("RSASSA-PSS");
|
||||
sig.setParameter(PSS_SPEC);
|
||||
sig.initSign(privateKey);
|
||||
sig.update(bytes);
|
||||
return sig.sign();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("RSA-PSS 서명 실패", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String algorithm() {
|
||||
return "PS256";
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package com.eactive.eai.authserver.jwt;
|
||||
|
||||
import java.security.Signature;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.security.spec.MGF1ParameterSpec;
|
||||
import java.security.spec.PSSParameterSpec;
|
||||
|
||||
import org.springframework.security.jwt.crypto.sign.InvalidSignatureException;
|
||||
import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
|
||||
|
||||
/**
|
||||
* PS256 (RSA-PSS + SHA-256) 서명 검증 구현체.
|
||||
* RFC 7518 §3.5 파라미터: Hash=SHA-256, MGF=MGF1/SHA-256, saltLen=32, trailer=0xBC
|
||||
*/
|
||||
public class RsaPssVerifier implements SignatureVerifier {
|
||||
|
||||
private static final PSSParameterSpec PSS_SPEC =
|
||||
new PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1);
|
||||
|
||||
private final RSAPublicKey publicKey;
|
||||
|
||||
public RsaPssVerifier(RSAPublicKey publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verify(byte[] content, byte[] signature) {
|
||||
try {
|
||||
Signature sig = Signature.getInstance("RSASSA-PSS");
|
||||
sig.setParameter(PSS_SPEC);
|
||||
sig.initVerify(publicKey);
|
||||
sig.update(content);
|
||||
if (!sig.verify(signature)) {
|
||||
throw new InvalidSignatureException("RSA-PSS 서명 검증 실패");
|
||||
}
|
||||
} catch (InvalidSignatureException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new InvalidSignatureException("RSA-PSS 서명 검증 실패: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String algorithm() {
|
||||
return "PS256";
|
||||
}
|
||||
}
|
||||
@@ -13,21 +13,15 @@ import com.eactive.eai.common.lifecycle.Lifecycle;
|
||||
import com.eactive.eai.common.lifecycle.LifecycleException;
|
||||
import com.eactive.eai.common.lifecycle.LifecycleListener;
|
||||
import com.eactive.eai.common.lifecycle.LifecycleSupport;
|
||||
import com.eactive.eai.common.session.SessionManager;
|
||||
import com.eactive.eai.common.session.SessionManagerForIgnite;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.mchange.v1.cachedstore.CachedStore.Manager;
|
||||
import com.openbanking.eai.common.token.AccessTokenVO;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* 1. 기능 : Access 토큰 정보를 DB로 부터 로딩하고 만료시 재발급 정보를 메모리에 관리하는 Manager 클래스
|
||||
@@ -61,22 +55,18 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
/**
|
||||
* 기동 여부
|
||||
*/
|
||||
private boolean started;
|
||||
|
||||
private boolean started;
|
||||
|
||||
/**
|
||||
* 토큰 정보를 저장하는 collection
|
||||
*/
|
||||
private HashMap<String, AccessTokenVO> tokens;
|
||||
|
||||
/**
|
||||
* 인증 정보를 저장하는 Map
|
||||
*/
|
||||
private Map<String, OutboundOAuthCredentialVo> outboundOAuthCredentialVos;
|
||||
|
||||
/** 어댑터그룹별로 보관할 토큰 발급 이력 건수 */
|
||||
private static final int ISSUE_HISTORY_SIZE = 10;
|
||||
|
||||
/** 이력에 남길 accessToken 앞자리 수 */
|
||||
private static final int UNMASKED_TOKEN_LENGTH = 8;
|
||||
|
||||
/** 어댑터그룹별 토큰 발급 이력. 이 노드 메모리에만 존재하며 재기동 시 사라진다. */
|
||||
private final Map<String, Deque<TokenIssueHistory>> issueHistories = new ConcurrentHashMap<>();
|
||||
|
||||
@Autowired
|
||||
OutboundOAuthCredentialDao outboundOAuthCredentialDao;
|
||||
|
||||
@@ -88,6 +78,7 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
private static final int TOKEN_SCHEDULER_THREAD_POOL_SIZE = 30;
|
||||
|
||||
public AccessTokenManagerByDB() {
|
||||
tokens = new HashMap<>();
|
||||
scheduledTasks = new ConcurrentHashMap<>();
|
||||
outboundOAuthCredentialVos = new HashMap<>();
|
||||
runningTasks = Collections.newSetFromMap(new ConcurrentHashMap<>());
|
||||
@@ -126,6 +117,7 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
lifecycle.fireLifecycleEvent(STARTING_EVENT, this);
|
||||
|
||||
try {
|
||||
tokens.clear();
|
||||
init();
|
||||
} catch(Exception e) {
|
||||
throw new LifecycleException(ExceptionUtil.getErrorCode(e,"RECEAICPM201"));
|
||||
@@ -176,6 +168,7 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
logger.error("Scheduler shutdown interrupted", e);
|
||||
}
|
||||
|
||||
this.tokens = null;
|
||||
started = false;
|
||||
|
||||
if (logger.isWarn()){
|
||||
@@ -247,37 +240,26 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
try {
|
||||
logger.debug("Executing token issuance for adapter group: {}", adapterGroupName);
|
||||
|
||||
SessionManager sessionManager = SessionManager.getInstance();
|
||||
|
||||
// 발급을 유발하지 않는 조회로 먼저 상태를 본다.
|
||||
// getOutboundAccessToken 은 "없거나 이미 만료" 일 때만 발급 함수를 부르기 때문에,
|
||||
// 만료 임박 판정을 그 안에 두면 도달하지 못한다.
|
||||
AccessTokenVO cached = sessionManager.peekOutboundAccessToken(adapterGroupName);
|
||||
long intervalTime = System.currentTimeMillis() + (credential.getIntervalSec() * 1000L);
|
||||
|
||||
if (cached == null) {
|
||||
logger.debug("Token not exists for adapter group: {}", adapterGroupName);
|
||||
sessionManager.getOutboundAccessToken(adapterGroupName,
|
||||
token -> issueToken(credential));
|
||||
|
||||
} else if (cached.getExpiration() == null) {
|
||||
// 만료시각이 없는 토큰은 갱신 시점을 판단할 수 없다.
|
||||
logger.debug("Token exists but expiration time is null for adapter group: {}",
|
||||
adapterGroupName);
|
||||
|
||||
} else if (cached.getExpiration().before(new Date(intervalTime))) {
|
||||
// 다음 스케줄 전에 만료되므로 미리 갱신한다.
|
||||
// 여러 노드가 동시에 들어와도 분산락 안에서 다시 확인해 한 번만 발급된다.
|
||||
// 다른 노드가 이미 넣어둔 토큰이 다음 틱까지 유효하면 발급하지 않는다.
|
||||
logger.debug("Token expires before next schedule : {}, expiration date: {}",
|
||||
adapterGroupName, cached.getExpiration());
|
||||
sessionManager.reissueOutboundAccessToken(adapterGroupName, cached.getAccessToken(),
|
||||
intervalTime, token -> issueToken(credential));
|
||||
// 현재 토큰 가져오기
|
||||
AccessTokenVO accessToken = tokens.get(adapterGroupName);
|
||||
|
||||
// 다음 스케줄 시간 계산
|
||||
long intervalTime = System.currentTimeMillis() + (credential.getIntervalSec() * 1000);
|
||||
// 토큰이 없거나, 다음 스케줄 시간 전에 만료될 경우 재발급
|
||||
if (accessToken == null) {
|
||||
issueToken(credential, false);
|
||||
} else if(accessToken.getExpiration() != null) {
|
||||
// 토큰이 있고 만료시간이 설정 된 경우
|
||||
if(accessToken.getExpiration().before(new Date(intervalTime))){
|
||||
// 다음 스케줄 전에 토큰이 만료되는 경우 재발급
|
||||
issueToken(credential, true);
|
||||
} else {
|
||||
// 토큰이 아직 유효한 경우
|
||||
logger.debug("Token still valid until next schedule for adapter group: {}, expiration date: {}", adapterGroupName, accessToken.getExpiration());
|
||||
}
|
||||
} else {
|
||||
logger.debug(
|
||||
"Token still valid until next schedule for adapter group: {}, expiration date: {}",
|
||||
adapterGroupName, cached.getExpiration());
|
||||
//토큰이 있지만 만료 시간이 없는 경우
|
||||
logger.debug("Token exists but expiration time is null for adapter group: {}", adapterGroupName);
|
||||
}
|
||||
|
||||
logger.debug("Token issuance completed for adapter group: {}", adapterGroupName);
|
||||
@@ -300,24 +282,19 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
}
|
||||
}
|
||||
|
||||
private AccessTokenVO issueToken(OutboundOAuthCredentialVo outboundOAuthCredentialVo) {
|
||||
String adapterGroupName = outboundOAuthCredentialVo.getAdapterGroupName();
|
||||
AccessTokenVO accessToken = null;
|
||||
try {
|
||||
|
||||
// boolean isExpired = true;
|
||||
|
||||
// if (tokens.containsKey(adapterGroupName)) {
|
||||
// AccessTokenVO accessToken = tokens.get(adapterGroupName);
|
||||
// isExpired = accessToken.isExpired();
|
||||
// }
|
||||
// logger.debug("Token status for adapter group: {}, expired: {}, isTokenExpiredWithinInterval: {}", adapterGroupName, isExpired, isTokenExpiredWithinInterval);
|
||||
private void issueToken(OutboundOAuthCredentialVo outboundOAuthCredentialVo, boolean isTokenExpiredWithinInterval) throws Exception {
|
||||
String adapterGroupName = outboundOAuthCredentialVo.getAdapterGroupName();
|
||||
boolean isExpired = true;
|
||||
|
||||
// if (isExpired || isTokenExpiredWithinInterval) {
|
||||
|
||||
|
||||
if (tokens.containsKey(adapterGroupName)) {
|
||||
AccessTokenVO accessToken = tokens.get(adapterGroupName);
|
||||
isExpired = accessToken.isExpired();
|
||||
}
|
||||
logger.debug("Token status for adapter group: {}, expired: {}, isTokenExpiredWithinInterval: {}", adapterGroupName, isExpired, isTokenExpiredWithinInterval);
|
||||
|
||||
if (isExpired || isTokenExpiredWithinInterval) {
|
||||
logger.info("Issuing new token for adapter group: {}", adapterGroupName);
|
||||
// tokens.remove(adapterGroupName);
|
||||
tokens.remove(adapterGroupName);
|
||||
|
||||
AdapterGroupVO gvo = AdapterManager.getInstance().getAdapterGroupVO(adapterGroupName);
|
||||
|
||||
@@ -329,47 +306,16 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
HttpClientAccessTokenServiceByDB service = HttpClientAccessTokenServiceFactoryByDB.createFactory(type);
|
||||
if(service != null) {
|
||||
logger.debug("Executing token service of type: {} for adapter group: {}", type, adapterGroupName);
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
try {
|
||||
accessToken = (AccessTokenVO) service.execute(adapterGroupName, properties,
|
||||
outboundOAuthCredentialVo);
|
||||
} catch (Exception e) {
|
||||
recordIssueHistory(adapterGroupName, TokenIssueHistory.TRIGGER_SCHEDULE, type, startTime, null,
|
||||
toFailReason(e));
|
||||
throw e;
|
||||
}
|
||||
|
||||
recordIssueHistory(adapterGroupName, TokenIssueHistory.TRIGGER_SCHEDULE, type, startTime,
|
||||
accessToken, null);
|
||||
|
||||
// 발급에 실패했는데도 빈 토큰을 반환하는 구현체가 있다. 그대로 캐시되면
|
||||
// 만료시각이 없어 재발급 대상이 되지 않으므로 실패로 처리한다.
|
||||
if (accessToken == null || StringUtils.isBlank(accessToken.getAccessToken())) {
|
||||
logger.error("Issued token is empty. type: {}, adapter group: {}", type, adapterGroupName);
|
||||
return null;
|
||||
}
|
||||
|
||||
AccessTokenVO accessToken = (AccessTokenVO) service.execute(adapterGroupName, properties, outboundOAuthCredentialVo);
|
||||
this.tokens.put(adapterGroupName, accessToken);
|
||||
logger.info("New token issued successfully for adapter group: {}", adapterGroupName);
|
||||
return accessToken;
|
||||
|
||||
} else {
|
||||
recordIssueHistory(adapterGroupName, TokenIssueHistory.TRIGGER_SCHEDULE, type,
|
||||
System.currentTimeMillis(), null, "토큰 발급 구현체를 찾을 수 없음");
|
||||
logger.warn("Token service not found for type: {} and adapter group: {}", type, adapterGroupName);
|
||||
}
|
||||
} else {
|
||||
recordIssueHistory(adapterGroupName, TokenIssueHistory.TRIGGER_SCHEDULE, null,
|
||||
System.currentTimeMillis(), null, "어댑터 설정을 찾을 수 없음");
|
||||
logger.warn("No valid adapter configuration found for adapter group: {}", adapterGroupName);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Task execution failed for adapter group: {}", adapterGroupName, e);
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
|
||||
}
|
||||
|
||||
public void stopToken(String adapterGroupName) {
|
||||
@@ -378,7 +324,7 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
task.cancel(true);
|
||||
logger.warn("Token task stopped for adapter group: {}", adapterGroupName);
|
||||
}
|
||||
SessionManager.getInstance().removeOutboundAccessToken(adapterGroupName);
|
||||
tokens.remove(adapterGroupName);
|
||||
logger.debug("Token removed for adapter group: {}", adapterGroupName);
|
||||
}
|
||||
|
||||
@@ -422,111 +368,6 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
lifecycle.removeLifecycleListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. 기능 : 토큰 발급 이력을 남긴다.
|
||||
* 2. 처리 개요 : 어댑터그룹별로 최근 ISSUE_HISTORY_SIZE 건만 노드 메모리에 보관한다.
|
||||
* 성공뿐 아니라 실패도 남긴다. 실패는 캐시에 아무것도 남지 않아 사후 추적이 어렵기 때문이다.
|
||||
* 3. 주의사항 : accessToken 은 앞 UNMASKED_TOKEN_LENGTH 자만 남겨 보관한다.
|
||||
*
|
||||
* @param adapterGroupName 어댑터그룹명
|
||||
* @param trigger SCHEDULE / RETRY
|
||||
* @param serviceClass 사용한 발급 구현체 클래스명
|
||||
* @param startTime 발급 시도 시각
|
||||
* @param accessToken 발급된 토큰. 실패 시 null.
|
||||
* @param failReason 실패 사유. 성공 시 null.
|
||||
**/
|
||||
private void recordIssueHistory(String adapterGroupName, String trigger, String serviceClass, long startTime,
|
||||
AccessTokenVO accessToken, String failReason) {
|
||||
String reason = failReason;
|
||||
String maskedToken = null;
|
||||
Date expiration = null;
|
||||
|
||||
if (reason == null) {
|
||||
if (accessToken == null || StringUtils.isBlank(accessToken.getAccessToken())) {
|
||||
reason = "발급된 토큰이 비어 있음";
|
||||
} else {
|
||||
maskedToken = maskToken(accessToken.getAccessToken());
|
||||
expiration = accessToken.getExpiration();
|
||||
}
|
||||
}
|
||||
|
||||
TokenIssueHistory history = new TokenIssueHistory(startTime, trigger, serviceClass, maskedToken, expiration,
|
||||
reason);
|
||||
|
||||
Deque<TokenIssueHistory> histories = issueHistories.computeIfAbsent(adapterGroupName,
|
||||
key -> new ArrayDeque<TokenIssueHistory>());
|
||||
|
||||
synchronized (histories) {
|
||||
histories.addFirst(history);
|
||||
while (histories.size() > ISSUE_HISTORY_SIZE) {
|
||||
histories.removeLast();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 예외 메시지가 비어 있는 경우를 대비해 클래스명이라도 이력에 남긴다. */
|
||||
private String toFailReason(Exception e) {
|
||||
return StringUtils.defaultIfBlank(e.getMessage(), e.getClass().getName());
|
||||
}
|
||||
|
||||
/** accessToken 앞자리만 남기고 마스킹한다. */
|
||||
private String maskToken(String accessToken) {
|
||||
if (accessToken.length() <= UNMASKED_TOKEN_LENGTH) {
|
||||
return StringUtils.repeat('*', accessToken.length());
|
||||
}
|
||||
return StringUtils.substring(accessToken, 0, UNMASKED_TOKEN_LENGTH) + "***";
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. 기능 : 어댑터그룹의 토큰 발급 이력을 최근 순으로 반환한다.
|
||||
* 2. 처리 개요 : 상태 조회 API 에서 사용한다. 이 노드에서 일어난 발급만 담긴다.
|
||||
*
|
||||
* @param adapterGroupName 어댑터그룹명
|
||||
* @return 최근 순 이력 목록. 없으면 빈 목록.
|
||||
**/
|
||||
public List<TokenIssueHistory> getIssueHistories(String adapterGroupName) {
|
||||
Deque<TokenIssueHistory> histories = issueHistories.get(adapterGroupName);
|
||||
if (histories == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
synchronized (histories) {
|
||||
return new ArrayList<>(histories);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. 기능 : 등록된 OAuth 인증 정보의 어댑터그룹명 목록을 반환한다.
|
||||
* 2. 처리 개요 : 상태 조회 API 에서 사용한다.
|
||||
*
|
||||
* @return 어댑터그룹명 목록
|
||||
**/
|
||||
public Set<String> getRegisteredAdapterGroupNames() {
|
||||
return new TreeSet<>(outboundOAuthCredentialVos.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. 기능 : 어댑터그룹의 OAuth 인증 정보를 반환한다.
|
||||
* 2. 처리 개요 : 상태 조회 API 에서 사용한다.
|
||||
* 3. 주의사항 : clientSecret 이 들어있으므로 응답 전문에 그대로 담지 말 것.
|
||||
*
|
||||
* @return OAuth 인증 정보. 등록돼 있지 않으면 null.
|
||||
**/
|
||||
public OutboundOAuthCredentialVo getOutboundOAuthCredentialVo(String adapterGroupName) {
|
||||
return outboundOAuthCredentialVos.get(adapterGroupName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. 기능 : 캐시에 있는 토큰만 조회한다.
|
||||
* 2. 처리 개요 : getAccessTokenVO() 와 달리 캐시에 없거나 만료됐어도 새로 발급하지 않는다.
|
||||
* 상태 조회가 토큰 발급을 유발하면 안 되므로 조회 API 는 이 메서드를 쓴다.
|
||||
*
|
||||
* @return 캐시에 있는 토큰. 없으면 null.
|
||||
* @exception UnsupportedOperationException 아웃바운드 토큰 캐시를 지원하지 않는 SessionManager 백엔드
|
||||
**/
|
||||
public AccessTokenVO peekAccessTokenVO(String adapterGroupName) {
|
||||
return SessionManager.getInstance().peekOutboundAccessToken(adapterGroupName);
|
||||
}
|
||||
|
||||
public boolean isOAuthCredentialRegistered(String adapterGroupName){
|
||||
OutboundOAuthCredentialVo outboundOAuthCredentialVo = outboundOAuthCredentialVos.get(adapterGroupName);
|
||||
return outboundOAuthCredentialVo != null && "Y".equals(outboundOAuthCredentialVo.getUseYn());
|
||||
@@ -541,28 +382,15 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
* @return Access 토큰 정보
|
||||
**/
|
||||
public AccessTokenVO getAccessTokenVO(String adapterGroupName) {
|
||||
AccessTokenVO accessToken = null;
|
||||
AccessTokenVO accessToken = null;
|
||||
|
||||
try {
|
||||
accessToken = tokens.get(adapterGroupName);
|
||||
} catch(Exception e) {
|
||||
if (logger.isError()) logger.error("토큰을 찾을 수 없습니다. ["+adapterGroupName+"] ", e);
|
||||
}
|
||||
|
||||
try {
|
||||
accessToken = SessionManager.getInstance().getOutboundAccessToken(adapterGroupName,
|
||||
new Function<AccessTokenVO, AccessTokenVO>() {
|
||||
|
||||
@Override
|
||||
public AccessTokenVO apply(AccessTokenVO t) {
|
||||
|
||||
OutboundOAuthCredentialVo outboundOAuthCredentialVo = outboundOAuthCredentialVos
|
||||
.get(adapterGroupName);
|
||||
|
||||
return issueToken(outboundOAuthCredentialVo);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
if (logger.isError())
|
||||
logger.error("토큰을 찾을 수 없습니다. [" + adapterGroupName + "] ", e);
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -575,9 +403,9 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
**/
|
||||
public String getAccessToken(String adapterGroupName) {
|
||||
String accessToken = null;
|
||||
|
||||
|
||||
try {
|
||||
accessToken = getAccessTokenVO(adapterGroupName).getAccessToken();
|
||||
accessToken = tokens.get(adapterGroupName).getAccessToken();
|
||||
} catch(Exception e) {
|
||||
if (logger.isError()) logger.error("토큰을 찾을 수 없습니다. ["+adapterGroupName+"] ", e);
|
||||
}
|
||||
@@ -607,50 +435,27 @@ public class AccessTokenManagerByDB implements Lifecycle {
|
||||
* 3. 주의사항
|
||||
*
|
||||
**/
|
||||
public AccessTokenVO retryAccessTokenVO(String adapterGroupName, Properties properties, String oldToken)
|
||||
throws Exception {
|
||||
public synchronized AccessTokenVO retryAccessTokenVO(String adapterGroupName, Properties properties, String oldToken) throws Exception {
|
||||
|
||||
AccessTokenVO accessToken = getAccessTokenVO(adapterGroupName);
|
||||
|
||||
if (isOAuthCredentialRegistered(adapterGroupName) == false) {
|
||||
throw new Exception("There is no OAuthCredentialRegistered information, or whether to use it is 'N'.");
|
||||
}
|
||||
// 동시 요청이 발생할 경우 synchronized 처리를 했기때문에 토큰을 다시 한번 체크한다.
|
||||
if (accessToken == null || accessToken.isExpired()
|
||||
|| StringUtils.equals(accessToken.getAccessToken(), oldToken)) {
|
||||
|
||||
String type = properties.getProperty("ADAPTER_TOKEN_ISSUING_CLIENT_TYPE");
|
||||
final HttpClientAccessTokenServiceByDB service = HttpClientAccessTokenServiceFactoryByDB.createFactory(type);
|
||||
|
||||
if (service == null) {
|
||||
logger.warn("Token service not found for type: {} and adapter group: {}", type, adapterGroupName);
|
||||
return getAccessTokenVO(adapterGroupName);
|
||||
}
|
||||
|
||||
final OutboundOAuthCredentialVo outboundOAuthCredentialVo = outboundOAuthCredentialVos.get(adapterGroupName);
|
||||
|
||||
// 클러스터 전역에서 한 번만 발급되도록 분산락 안에서 처리한다. 발급 결과는 캐시에 반영되므로
|
||||
// 뒤따르는 거래는 재발급하지 않는다. 구현체는 호출자가 넘긴 어댑터 속성 기준으로 고른다.
|
||||
// 거부된 토큰을 바꾸는 것이 목적이므로 만료 기준 검사는 하지 않고 oldToken 비교만 한다.
|
||||
// (만료 전이어도 상대 기관이 거부한 상황이다)
|
||||
return SessionManager.getInstance().reissueOutboundAccessToken(adapterGroupName, oldToken, 0L,
|
||||
new Function<AccessTokenVO, AccessTokenVO>() {
|
||||
|
||||
@Override
|
||||
public AccessTokenVO apply(AccessTokenVO currentToken) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
try {
|
||||
logger.info("Reissuing token for adapter group: {}, type: {}", adapterGroupName, type);
|
||||
|
||||
AccessTokenVO issued = (AccessTokenVO) service.execute(adapterGroupName, properties,
|
||||
outboundOAuthCredentialVo);
|
||||
|
||||
recordIssueHistory(adapterGroupName, TokenIssueHistory.TRIGGER_RETRY, type, startTime,
|
||||
issued, null);
|
||||
|
||||
return issued;
|
||||
} catch (Exception e) {
|
||||
recordIssueHistory(adapterGroupName, TokenIssueHistory.TRIGGER_RETRY, type, startTime,
|
||||
null, toFailReason(e));
|
||||
logger.error("Token reissue failed for adapter group: {}", adapterGroupName, e);
|
||||
return null;
|
||||
}
|
||||
if(isOAuthCredentialRegistered(adapterGroupName) == false){
|
||||
throw new Exception("There is no OAuthCredentialRegistered information, or whether to use it is 'N'.");
|
||||
}
|
||||
});
|
||||
}
|
||||
String type = properties.getProperty("ADAPTER_TOKEN_ISSUING_CLIENT_TYPE");
|
||||
|
||||
HttpClientAccessTokenServiceByDB service = HttpClientAccessTokenServiceFactoryByDB.createFactory(type);
|
||||
OutboundOAuthCredentialVo outboundOAuthCredentialVo = outboundOAuthCredentialVos.get(adapterGroupName);
|
||||
if(service != null) {
|
||||
accessToken = (AccessTokenVO) service.execute(adapterGroupName, properties, outboundOAuthCredentialVo);
|
||||
}
|
||||
tokens.put(adapterGroupName, accessToken);
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
package com.eactive.eai.common.authoutbound;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 아웃바운드 OAuth 토큰 발급 이력 한 건.
|
||||
*
|
||||
* 진단 목적이므로 성공뿐 아니라 실패도 남긴다. accessToken 은 마스킹된 값만 담는다.
|
||||
* 이 인스턴스는 노드 메모리에만 존재하며 재기동 시 사라진다.
|
||||
*/
|
||||
@Getter
|
||||
public class TokenIssueHistory {
|
||||
|
||||
/** 스케줄러의 주기 발급 */
|
||||
public static final String TRIGGER_SCHEDULE = "SCHEDULE";
|
||||
|
||||
/** 거래 중 재발급 */
|
||||
public static final String TRIGGER_RETRY = "RETRY";
|
||||
|
||||
private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
|
||||
|
||||
/** 발급을 시도한 시각 */
|
||||
private final String issuedAt;
|
||||
|
||||
/** SCHEDULE | RETRY */
|
||||
private final String trigger;
|
||||
|
||||
/** 사용한 발급 구현체 클래스명 */
|
||||
private final String serviceClass;
|
||||
|
||||
private final boolean success;
|
||||
|
||||
/** 발급에 걸린 시간(ms) */
|
||||
private final long elapsedMs;
|
||||
|
||||
/** 앞 8자만 남긴 accessToken. 실패 시 null. */
|
||||
private final String accessTokenMasked;
|
||||
|
||||
/** 발급된 토큰의 만료 시각. 실패 시 null. */
|
||||
private final String expiration;
|
||||
|
||||
/** 실패 사유. 성공 시 null. */
|
||||
private final String failReason;
|
||||
|
||||
TokenIssueHistory(long startTime, String trigger, String serviceClass, String accessTokenMasked, Date expiration,
|
||||
String failReason) {
|
||||
SimpleDateFormat formatter = new SimpleDateFormat(DATE_FORMAT);
|
||||
|
||||
this.issuedAt = formatter.format(new Date(startTime));
|
||||
this.trigger = trigger;
|
||||
this.serviceClass = serviceClass;
|
||||
this.elapsedMs = System.currentTimeMillis() - startTime;
|
||||
this.failReason = failReason;
|
||||
this.success = failReason == null;
|
||||
this.accessTokenMasked = accessTokenMasked;
|
||||
this.expiration = expiration == null ? null : formatter.format(expiration);
|
||||
}
|
||||
}
|
||||
+16
-13
@@ -17,6 +17,10 @@ import com.eactive.eai.common.lifecycle.LifecycleSupport;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.custom.alarm.AlarmStateManager;
|
||||
import com.eactive.eai.custom.alarm.condition.AlarmCondition;
|
||||
import com.eactive.eai.custom.alarm.event.AlarmEvent;
|
||||
import com.eactive.eai.custom.alarm.key.CoreAlarmKey;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
@@ -41,7 +45,7 @@ public class TypedCircuitBreakerManager implements Lifecycle {
|
||||
TypedCircuitBreakerVO defaultConfigVo;
|
||||
HashMap<String, TypedCircuitBreakerVO> idCBConfigMap = new HashMap<>();
|
||||
HashMap<String, TypedCircuitBreakerVO> apiIdCBConfigMap = new HashMap<>();
|
||||
// HashMap<String, TypedCircuitBreakerVO> urlCBConfigMap = new HashMap<>();
|
||||
HashMap<String, TypedCircuitBreakerVO> urlCBConfigMap = new HashMap<>();
|
||||
private boolean started;
|
||||
|
||||
@Autowired
|
||||
@@ -112,7 +116,7 @@ public class TypedCircuitBreakerManager implements Lifecycle {
|
||||
CircuitBreakerConfig circuitBreakerConfig = this.createCircuitBreakerConfig(vo);
|
||||
vo.setCircuitBreakerConfig(circuitBreakerConfig);
|
||||
this.apiIdCBConfigMap.put(vo.getName(), vo);
|
||||
this.idCBConfigMap.put(vo.getId(), vo);
|
||||
this.idCBConfigMap.remove(vo.getId(), vo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,7 +340,7 @@ public class TypedCircuitBreakerManager implements Lifecycle {
|
||||
switch (event.getEventType()) {
|
||||
case ERROR :
|
||||
TypedCircuitBreakerManager.logger
|
||||
.error("CircuitBreaker ERROR event occurred. CircuitBreakerEvent={}" + event);
|
||||
.error("CirbuitBreaker ERROR event occurred. CircuitBreakerEvent={}" + event);
|
||||
break;
|
||||
case STATE_TRANSITION :
|
||||
if (event instanceof CircuitBreakerOnStateTransitionEvent) {
|
||||
@@ -345,7 +349,7 @@ public class TypedCircuitBreakerManager implements Lifecycle {
|
||||
CircuitBreaker.State toState = stateTransition.getToState();
|
||||
CircuitBreaker.State fromState = stateTransition.getFromState();
|
||||
TypedCircuitBreakerManager.logger.error(
|
||||
"CircuitBreaker STATE_TRANSITION event occurred. {} : {} -> {}. CircuitBreakerEvent={}, UrlCircuitBreakerVO={}",
|
||||
"CirbuitBreaker STATE_TRANSITION event occurred. {} : {} -> {}. CircuitBreakerEvent={}, UrlCircuitBreakerVO={}",
|
||||
new Object[]{event.getCircuitBreakerName(), fromState, toState, event, this.vo});
|
||||
// if (!toState.equals(State.OPEN) && !toState.equals(State.HALF_OPEN)
|
||||
// && !toState.equals(State.CLOSED)) {
|
||||
@@ -355,19 +359,18 @@ public class TypedCircuitBreakerManager implements Lifecycle {
|
||||
// } else {
|
||||
// this.sendAlert(stateTransitionEvent);
|
||||
// }
|
||||
|
||||
// AlarmEvent alarmEvent = AlarmEvent.builder().key(CoreAlarmKey.CircuitBreaker)
|
||||
// .condition(AlarmCondition.builder().build())
|
||||
// .message(String.format("CircuitBreaker state changed. apiId=%s, %s -> %s", event.getCircuitBreakerName(), fromState, toState))
|
||||
// .build();
|
||||
//
|
||||
// AlarmStateManager.getAlarmStateManager().onEvnet(alarmEvent);
|
||||
AlarmEvent alarmEvent = AlarmEvent.builder().key(CoreAlarmKey.CircuitBreaker)
|
||||
.condition(AlarmCondition.builder().build())
|
||||
.message(String.format("CircuitBreaker state changed. apiId=%s, %s -> %s", event.getCircuitBreakerName(), fromState, toState))
|
||||
.build();
|
||||
|
||||
AlarmStateManager.getAlarmStateManager().onEvnet(alarmEvent);
|
||||
|
||||
TypedCircuitBreakerManager.logger.warn("CircuitBreaker state changed. apiId={}, {} -> {}", event.getCircuitBreakerName(), fromState, toState);
|
||||
logger.warn("CircuitBreaker state changed. apiId={}, {} -> {}", event.getCircuitBreakerName(), fromState, toState);
|
||||
|
||||
} else {
|
||||
TypedCircuitBreakerManager.logger.error(
|
||||
"CircuitBreaker STATE_TRANSITION event occurred. CircuitBreakerEvent={}, UrlCircuitBreakerVO={}",
|
||||
"CirbuitBreaker STATE_TRANSITION event occurred. CircuitBreakerEvent={}, UrlCircuitBreakerVO={}",
|
||||
new Object[]{event, this.vo});
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
package com.eactive.eai.common.exception;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.json.simple.JSONValue;
|
||||
|
||||
import com.eactive.eai.adapter.AdapterGroupVO;
|
||||
import com.eactive.eai.adapter.AdapterManager;
|
||||
import com.eactive.eai.adapter.AdapterPropManager;
|
||||
import com.eactive.eai.adapter.AdapterVO;
|
||||
import com.eactive.eai.adapter.handler.AdapterErrorMessageHandler;
|
||||
import com.eactive.eai.adapter.handler.AdapterErrorMessageHandlerFactory;
|
||||
import com.eactive.eai.common.EAIKeys;
|
||||
import com.eactive.eai.common.logger.EAILogSender;
|
||||
import com.eactive.eai.common.message.EAIMessage;
|
||||
@@ -60,9 +56,6 @@ public class ExceptionHandler {
|
||||
}
|
||||
|
||||
public static EAIMessage handle(EAIMessage eaiMessage) {
|
||||
Properties prop = eaiMessage.getCallProp();
|
||||
prop.setProperty("API_INTERNAL_ERROR_CODE", eaiMessage.getRspErrCd());
|
||||
|
||||
String inAdapterGroupName = eaiMessage.getSngSysItfTp();
|
||||
AdapterManager manager = AdapterManager.getInstance();
|
||||
AdapterGroupVO group = manager.getAdapterGroupVO(inAdapterGroupName);
|
||||
@@ -756,55 +749,6 @@ public class ExceptionHandler {
|
||||
String serviceType, String psvItfTp, String charset) {
|
||||
String error = null;
|
||||
EAIMessage resultEAIMessage = null;
|
||||
|
||||
// AdapterErrorMessageHandler(TemplateAdapterErrorMsgHandler) 설정이 있으면 우선 호출
|
||||
try {
|
||||
AdapterGroupVO inboundAdapterGroupVO = AdapterManager.getInstance().getAdapterGroupVO(sngSysItfTp);
|
||||
if (inboundAdapterGroupVO != null) {
|
||||
AdapterVO inboundAdapterVO = inboundAdapterGroupVO.nextAdapterVO();
|
||||
if (inboundAdapterVO != null) {
|
||||
String errorHandlerClass = AdapterPropManager.getInstance().getProperty(
|
||||
inboundAdapterVO.getPropGroupName(), "ERR_MSG_HANDLER");
|
||||
if (StringUtils.isNotBlank(errorHandlerClass)) {
|
||||
if (logger.isInfo())
|
||||
logger.info("ExceptionHandler] errorSyncSend ERR_MSG_HANDLER=" + errorHandlerClass);
|
||||
AdapterErrorMessageHandler handler = AdapterErrorMessageHandlerFactory.createHandler(errorHandlerClass);
|
||||
if (handler != null) {
|
||||
// 표준전문에 에러코드/에러메시지 세팅
|
||||
try {
|
||||
StandardMessageManager standardManager = StandardMessageManager.getInstance();
|
||||
standardManager.getMessageCoordinator().coordinateSetStandardMessageError(
|
||||
eaiMessage.getStandardMessage(), eaiMessage.getMapper(), errCode, errMsg);
|
||||
} catch (Exception e) {
|
||||
if (logger.isWarn())
|
||||
logger.warn("ExceptionHandler] errorSyncSend coordinateSetStandardMessageError 처리 실패: " + e.getMessage());
|
||||
}
|
||||
|
||||
Object responseObj = handler.generateNonStandardInternalErrorResponseMessage(
|
||||
sngSysItfTp, inboundAdapterVO.getName(), null, tmpMsg, eaiMessage);
|
||||
if (responseObj != null) {
|
||||
resultEAIMessage = eaiMessage;
|
||||
try {
|
||||
resultEAIMessage.getStandardMessage().setBizData(responseObj, charset);
|
||||
} catch (Exception e) {
|
||||
logger.error("setBizData error", e);
|
||||
}
|
||||
resultEAIMessage.getMapper().setResponseType(
|
||||
resultEAIMessage.getStandardMessage(), STDMessageKeys.RESPONSE_TYPE_CODE_E);
|
||||
resultEAIMessage.setOrgRspErrCd(resultEAIMessage.getRspErrCd());
|
||||
resultEAIMessage.setRspErrCd(EAIMessageKeys.EAI_SUCCESS_CODE, false);
|
||||
|
||||
return resultEAIMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (logger.isWarn())
|
||||
logger.warn("ExceptionHandler] errorSyncSend AdapterErrorMessageHandler 처리 실패: " + e.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
if (transClassify) {
|
||||
if (logger.isInfo()) {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.eactive.eai.common.hsm;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* HsmCryptoService 키 캐시의 진단 정보. 키 원본(바이트/인코딩)은 절대 포함하지 않고
|
||||
* alias, 종류, 알고리즘, 캐시 시각/만료 여부만 노출한다.
|
||||
*/
|
||||
@Data
|
||||
public class HsmCachedKeyInfo {
|
||||
|
||||
/** SECRET(대칭키) / PUBLIC(공개키) */
|
||||
String keyType;
|
||||
|
||||
/** HSM KeyStore alias */
|
||||
String alias;
|
||||
|
||||
/** AES, RSA 등 */
|
||||
String algorithm;
|
||||
|
||||
/** 캐시에 등록된 시각 (epoch millis) */
|
||||
long cachedAt;
|
||||
|
||||
/** 캐시 등록 후 경과 시간 (millis) */
|
||||
long ageMillis;
|
||||
|
||||
/** CACHE_TTL_SEC 기준 만료 여부. 만료되어도 HSM 장애 시 fallback 으로 사용된다. */
|
||||
boolean expired;
|
||||
|
||||
/** 키 길이(바이트). non-extractable 등으로 알 수 없으면 -1. 키 값 자체는 노출하지 않는다. */
|
||||
int keyLength;
|
||||
}
|
||||
@@ -1,371 +0,0 @@
|
||||
package com.eactive.eai.common.hsm;
|
||||
|
||||
import java.beans.PropertyChangeEvent;
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Provider;
|
||||
import java.security.PublicKey;
|
||||
import java.security.cert.Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
/**
|
||||
* SafeNet ProtectServer HSM 암복호화 서비스 (JDK 8)
|
||||
*
|
||||
* [키 조회]
|
||||
* getPublicKey - 공개키 반환 (외부 노출). 최초 1회만 HSM 통신, 이후 캐시 반환.
|
||||
* getSecretKey - AES 대칭키 반환 (CKA_EXTRACTABLE=true 필요). 최초 1회만 HSM 통신, 이후 캐시 반환.
|
||||
*
|
||||
* [암복호화]
|
||||
* encryptRsa - 공개키로 JVM 소프트웨어 암호화 (HSM 불필요)
|
||||
* decryptRsa - alias 기반, HSM 내부 복호화 (매 호출마다 HSM 통신 발생 - 불가피)
|
||||
* encryptAes - SecretKey 객체로 AES/CBC 암호화
|
||||
* decryptAes - SecretKey 객체로 AES/CBC 복호화
|
||||
*/
|
||||
@Service
|
||||
public class HsmCryptoService implements PropertyChangeListener {
|
||||
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
|
||||
|
||||
private static final String RSA_ALGORITHM = "RSA/ECB/PKCS1Padding";
|
||||
private static final String AES_ALGORITHM = "AES/CBC/PKCS5Padding";
|
||||
|
||||
private static final String PROP_GROUP = "HSM";
|
||||
private static final String PROP_CACHE_RELOAD_YN = "CACHE_RELOAD_YN";
|
||||
|
||||
private static final String PROP_CACHE_TTL_SEC = "CACHE_TTL_SEC";
|
||||
private static long CACHE_TTL_MS = 10 * 60 * 1000; // 10분, 필요시 PropManager로 외부화
|
||||
|
||||
private static class CachedKey<T> {
|
||||
final T key;
|
||||
final long cachedAt;
|
||||
|
||||
CachedKey(T key) {
|
||||
this.key = key;
|
||||
this.cachedAt = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
boolean isExpired() {
|
||||
return System.currentTimeMillis() - cachedAt > CACHE_TTL_MS;
|
||||
}
|
||||
}
|
||||
|
||||
// HSM 통신 최소화: 최초 1회 조회 후 JVM 메모리에 캐싱
|
||||
private final ConcurrentHashMap<String, CachedKey<SecretKey>> secretKeyCache = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, CachedKey<PublicKey>> publicKeyCache = new ConcurrentHashMap<>();
|
||||
|
||||
@Autowired
|
||||
private PropManager propManager;
|
||||
|
||||
@PostConstruct
|
||||
public void registerPropertyChangeListener() {
|
||||
propManager.addPropertyChangeListener(this);
|
||||
logger.warn("HsmCryptoService] PropManager PropertyChangeListener 등록 완료");
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리포털에서 PropManager.reload("HSM") 호출 시 이벤트를 수신한다.
|
||||
* HSM.CACHE_RELOAD_YN = Y 이면 키 캐시를 초기화한다.
|
||||
*/
|
||||
@Override
|
||||
public void propertyChange(PropertyChangeEvent evt) {
|
||||
if (!PROP_GROUP.equals(evt.getPropertyName())) {
|
||||
return;
|
||||
}
|
||||
String reloadYn = propManager.getProperty(PROP_GROUP, PROP_CACHE_RELOAD_YN, "N");
|
||||
if ("Y".equalsIgnoreCase(reloadYn)) {
|
||||
clearKeyCache();
|
||||
}
|
||||
|
||||
String propCacheTtlSec = propManager.getProperty(PROP_GROUP, PROP_CACHE_TTL_SEC, "600");
|
||||
CACHE_TTL_MS = (Integer.parseInt(propCacheTtlSec.trim())) * 1000; // 10분, 필요시 PropManager로 외부화
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 키 조회 (외부 노출)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* HSM KeyStore 에서 공개키를 반환합니다. 최초 1회만 HSM 통신하고 이후 캐시를 반환합니다.
|
||||
* HSM 장애 시 만료된 캐시가 있으면 그것을 반환하여 서비스 연속성을 유지합니다.
|
||||
*/
|
||||
public PublicKey getPublicKey(String keyAlias) throws HsmException {
|
||||
// 1. 유효한 캐시 즉시 반환 (HSM 상태 무관)
|
||||
CachedKey<PublicKey> cached = publicKeyCache.get(keyAlias);
|
||||
if (cached != null && !cached.isExpired()) {
|
||||
return cached.key;
|
||||
}
|
||||
|
||||
// 2. 캐시 미스 또는 만료 → HSM 갱신 시도
|
||||
if (HsmManager.getInstance().isReady()) {
|
||||
try {
|
||||
Certificate cert = HsmManager.getInstance().getKeyStore().getCertificate(keyAlias);
|
||||
if (cert == null) {
|
||||
throw new HsmException("인증서를 찾을 수 없습니다. alias=" + keyAlias);
|
||||
}
|
||||
PublicKey key = cert.getPublicKey();
|
||||
publicKeyCache.put(keyAlias, new CachedKey<>(key));
|
||||
logger.warn("HsmCryptoService] 공개키 캐시 등록: alias=" + keyAlias);
|
||||
return key;
|
||||
} catch (HsmException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
logger.warn("HsmCryptoService] 공개키 HSM 조회 실패: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 3. HSM 조회 불가 → 만료 캐시 fallback
|
||||
if (cached != null) {
|
||||
logger.warn("HsmCryptoService] HSM 장애, 만료 공개키 캐시 fallback: alias=" + keyAlias);
|
||||
return cached.key;
|
||||
}
|
||||
|
||||
throw new HsmException("공개키 조회 불가: HSM 장애이며 캐시도 없습니다. alias=" + keyAlias);
|
||||
}
|
||||
|
||||
/**
|
||||
* HSM KeyStore 에서 AES 대칭키를 반환합니다. 최초 1회만 HSM 통신하고 이후 캐시를 반환합니다.
|
||||
* HSM 키 생성 시 CKA_EXTRACTABLE=true (ctkmu -x 플래그) 로 생성된 키만 반환됩니다.
|
||||
*
|
||||
* [캐싱 전략]
|
||||
* HSM 에서 가져온 P11SecretKey(PKCS11 핸들 래퍼)를 그대로 캐싱하면, HSM Provider 가
|
||||
* 재초기화될 때 세션 무효화로 인해 캐시된 키를 사용한 Cipher 연산이 실패한다.
|
||||
* 따라서 getEncoded() 로 키 바이트를 추출하여 SecretKeySpec(JVM 메모리 키) 으로 변환 후
|
||||
* 캐싱한다. encryptAes/decryptAes 는 이미 JVM 소프트웨어 Cipher 를 사용하므로,
|
||||
* HSM Provider 상태와 완전히 독립적으로 동작한다.
|
||||
*
|
||||
* HSM 장애 시 만료된 캐시가 있으면 그것을 반환하여 서비스 연속성을 유지합니다.
|
||||
*/
|
||||
public SecretKey getSecretKey(String keyAlias) throws HsmException {
|
||||
// 1. 유효한 캐시 즉시 반환 (HSM 상태 무관)
|
||||
CachedKey<SecretKey> cached = secretKeyCache.get(keyAlias);
|
||||
if (cached != null && !cached.isExpired()) {
|
||||
return cached.key;
|
||||
}
|
||||
|
||||
// 2. 캐시 미스 또는 만료 → HSM 갱신 시도
|
||||
if (HsmManager.getInstance().isReady()) {
|
||||
try {
|
||||
KeyStore keyStore = HsmManager.getInstance().getKeyStore();
|
||||
java.security.Key key = keyStore.getKey(keyAlias, null);
|
||||
if (key == null) {
|
||||
throw new HsmException("키를 찾을 수 없습니다. alias=" + keyAlias);
|
||||
}
|
||||
SecretKey secretKey = (SecretKey) key;
|
||||
|
||||
// P11SecretKey → SecretKeySpec 변환: HSM Provider 의존성 제거
|
||||
// getEncoded() 가 null 이면 non-extractable 키이므로 원본 유지
|
||||
byte[] keyBytes = secretKey.getEncoded();
|
||||
if (keyBytes != null) {
|
||||
secretKey = new SecretKeySpec(keyBytes, secretKey.getAlgorithm());
|
||||
}
|
||||
|
||||
secretKeyCache.put(keyAlias, new CachedKey<>(secretKey));
|
||||
logger.warn("HsmCryptoService] 대칭키 캐시 등록(갱신): alias=" + keyAlias);
|
||||
return secretKey;
|
||||
|
||||
} catch (HsmException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
logger.warn("HsmCryptoService] 대칭키 HSM 조회 실패: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 3. HSM 조회 불가 → 만료 캐시 fallback
|
||||
if (cached != null) {
|
||||
logger.warn("HsmCryptoService] HSM 장애, 만료 대칭키 캐시 fallback: alias=" + keyAlias);
|
||||
return cached.key;
|
||||
}
|
||||
|
||||
throw new HsmException("키 조회 불가: HSM 장애이며 캐시도 없습니다. alias=" + keyAlias);
|
||||
}
|
||||
|
||||
/** 캐시를 비웁니다. HSM 키 교체 후 재로드가 필요할 때 호출합니다. */
|
||||
public void clearKeyCache() {
|
||||
secretKeyCache.clear();
|
||||
publicKeyCache.clear();
|
||||
logger.warn("HsmCryptoService] 키 캐시 초기화 완료");
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 캐싱된 키 목록의 진단 정보를 반환합니다. 키 원본은 포함하지 않습니다.
|
||||
* HSM 상태 조회 API(/manage/hsm/status)에서 사용합니다.
|
||||
*/
|
||||
public List<HsmCachedKeyInfo> getCachedKeyInfos() {
|
||||
List<HsmCachedKeyInfo> infos = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, CachedKey<SecretKey>> entry : secretKeyCache.entrySet()) {
|
||||
SecretKey key = entry.getValue().key;
|
||||
byte[] encoded = (key == null) ? null : key.getEncoded();
|
||||
infos.add(toInfo("SECRET", entry.getKey(), entry.getValue(),
|
||||
(key == null) ? null : key.getAlgorithm(),
|
||||
(encoded == null) ? -1 : encoded.length));
|
||||
}
|
||||
|
||||
for (Map.Entry<String, CachedKey<PublicKey>> entry : publicKeyCache.entrySet()) {
|
||||
PublicKey key = entry.getValue().key;
|
||||
byte[] encoded = (key == null) ? null : key.getEncoded();
|
||||
infos.add(toInfo("PUBLIC", entry.getKey(), entry.getValue(),
|
||||
(key == null) ? null : key.getAlgorithm(),
|
||||
(encoded == null) ? -1 : encoded.length));
|
||||
}
|
||||
|
||||
infos.sort(Comparator.comparing(HsmCachedKeyInfo::getKeyType)
|
||||
.thenComparing(HsmCachedKeyInfo::getAlias));
|
||||
return infos;
|
||||
}
|
||||
|
||||
private HsmCachedKeyInfo toInfo(String keyType, String alias, CachedKey<?> cached,
|
||||
String algorithm, int keyLength) {
|
||||
HsmCachedKeyInfo info = new HsmCachedKeyInfo();
|
||||
info.setKeyType(keyType);
|
||||
info.setAlias(alias);
|
||||
info.setAlgorithm(algorithm);
|
||||
info.setCachedAt(cached.cachedAt);
|
||||
info.setAgeMillis(System.currentTimeMillis() - cached.cachedAt);
|
||||
info.setExpired(cached.isExpired());
|
||||
info.setKeyLength(keyLength);
|
||||
return info;
|
||||
}
|
||||
|
||||
/** 현재 적용된 키 캐시 TTL(밀리초). HSM.CACHE_TTL_SEC 프로퍼티로 변경된다. */
|
||||
public long getCacheTtlMs() {
|
||||
return CACHE_TTL_MS;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// RSA
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* RSA 암호화 - HSM 인증서의 공개키로 JVM 소프트웨어 암호화.
|
||||
* 공개키는 HSM 에서 추출하므로 HsmManager 가 초기화되어 있어야 합니다.
|
||||
*/
|
||||
public byte[] encryptRsa(String keyAlias, byte[] plaintext) throws HsmException {
|
||||
return encryptRsa(getPublicKey(keyAlias), plaintext);
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA 암호화 - 전달받은 공개키로 JVM 소프트웨어 암호화.
|
||||
* HSM 없이도 호출 가능합니다.
|
||||
*/
|
||||
public byte[] encryptRsa(PublicKey publicKey, byte[] plaintext) throws HsmException {
|
||||
try {
|
||||
// Provider 미명시 → JVM 소프트웨어 Provider(SunRsaSign) 자동 선택
|
||||
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
|
||||
return cipher.doFinal(plaintext);
|
||||
} catch (Exception e) {
|
||||
throw new HsmException("RSA 암호화 실패: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA 복호화 - 개인키 핸들을 HSM 에서 가져와 HSM 내부에서 복호화.
|
||||
* HsmManager 가 초기화되지 않은 경우 JVM 소프트웨어로 fallback 합니다.
|
||||
*
|
||||
* @param keyAlias HSM KeyStore 의 개인키 별칭
|
||||
* @param pin HSM 슬롯 PIN (null 이면 HsmManager 초기화 시 사용한 PIN 재사용)
|
||||
* @param ciphertext 암호문 바이트
|
||||
*/
|
||||
public byte[] decryptRsa(String keyAlias, char[] pin, byte[] ciphertext) throws HsmException {
|
||||
try {
|
||||
HsmManager hsmManager = HsmManager.getInstance();
|
||||
|
||||
if (hsmManager.isReady()) {
|
||||
// HSM 내부 복호화: pkcs11Provider 명시 필수
|
||||
Provider pkcs11Provider = hsmManager.getPkcs11Provider();
|
||||
java.security.Key key = hsmManager.getKeyStore().getKey(keyAlias, pin);
|
||||
if (!(key instanceof PrivateKey)) {
|
||||
throw new HsmException("개인키를 찾을 수 없습니다. alias=" + keyAlias);
|
||||
}
|
||||
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM, pkcs11Provider);
|
||||
cipher.init(Cipher.DECRYPT_MODE, (PrivateKey) key);
|
||||
return cipher.doFinal(ciphertext);
|
||||
|
||||
} else {
|
||||
throw new HsmException("RSA 복호화 불가: HsmManager 가 초기화되지 않았습니다. alias=" + keyAlias);
|
||||
}
|
||||
|
||||
} catch (HsmException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new HsmException("RSA 복호화 실패: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// AES
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* AES/CBC/PKCS5Padding 암호화.
|
||||
* extractable 키: provider=null 로 JVM 소프트웨어 암호화.
|
||||
* non-extractable 키: provider 에 pkcs11Provider 를 전달해야 합니다.
|
||||
*
|
||||
* @param secretKey 대칭키 (HSM 추출 키 또는 소프트웨어 키)
|
||||
* @param iv 초기화 벡터 (16바이트)
|
||||
* @param plaintext 평문 바이트
|
||||
* @param provider null 이면 JVM 기본 Provider 사용
|
||||
*/
|
||||
public byte[] encryptAes(SecretKey secretKey, byte[] iv, byte[] plaintext, Provider provider) throws HsmException {
|
||||
try {
|
||||
Cipher cipher = (provider != null)
|
||||
? Cipher.getInstance(AES_ALGORITHM, provider)
|
||||
: Cipher.getInstance(AES_ALGORITHM);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
|
||||
return cipher.doFinal(plaintext);
|
||||
} catch (Exception e) {
|
||||
throw new HsmException("AES 암호화 실패: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/** AES 암호화 - extractable 키 전용 (JVM 소프트웨어 Provider 사용). */
|
||||
public byte[] encryptAes(SecretKey secretKey, byte[] iv, byte[] plaintext) throws HsmException {
|
||||
return encryptAes(secretKey, iv, plaintext, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* AES/CBC/PKCS5Padding 복호화.
|
||||
* extractable 키: provider=null 로 JVM 소프트웨어 복호화.
|
||||
* non-extractable 키: provider 에 pkcs11Provider 를 전달해야 합니다.
|
||||
*
|
||||
* @param secretKey 대칭키 (HSM 추출 키 또는 소프트웨어 키)
|
||||
* @param iv 초기화 벡터 (16바이트)
|
||||
* @param ciphertext 암호문 바이트
|
||||
* @param provider null 이면 JVM 기본 Provider 사용
|
||||
*/
|
||||
public byte[] decryptAes(SecretKey secretKey, byte[] iv, byte[] ciphertext, Provider provider) throws HsmException {
|
||||
try {
|
||||
Cipher cipher = (provider != null)
|
||||
? Cipher.getInstance(AES_ALGORITHM, provider)
|
||||
: Cipher.getInstance(AES_ALGORITHM);
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
|
||||
return cipher.doFinal(ciphertext);
|
||||
} catch (Exception e) {
|
||||
throw new HsmException("AES 복호화 실패: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/** AES 복호화 - extractable 키 전용 (JVM 소프트웨어 Provider 사용). */
|
||||
public byte[] decryptAes(SecretKey secretKey, byte[] iv, byte[] ciphertext) throws HsmException {
|
||||
return decryptAes(secretKey, iv, ciphertext, null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.eactive.eai.common.hsm;
|
||||
|
||||
public class HsmException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public HsmException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public HsmException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
}
|
||||
@@ -1,844 +0,0 @@
|
||||
package com.eactive.eai.common.hsm;
|
||||
|
||||
import java.beans.PropertyChangeEvent;
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Files;
|
||||
import java.security.AuthProvider;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Provider;
|
||||
import java.security.Security;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.eactive.eai.agent.encryption.EncryptionManager;
|
||||
import com.eactive.eai.common.exception.ExceptionUtil;
|
||||
import com.eactive.eai.common.lifecycle.Lifecycle;
|
||||
import com.eactive.eai.common.lifecycle.LifecycleException;
|
||||
import com.eactive.eai.common.lifecycle.LifecycleListener;
|
||||
import com.eactive.eai.common.lifecycle.LifecycleSupport;
|
||||
import com.eactive.eai.common.property.PropGroupVO;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
/**
|
||||
* SafeNet ProtectServer HSM 연동 관리자 (JDK 8 / SunPKCS11)
|
||||
*
|
||||
* PropManager 그룹 "HSM" 에서 읽는 키:
|
||||
* PKCS11_CONFIG - primary pkcs11.cfg 파일 내용
|
||||
* PIN - primary 슬롯 PIN
|
||||
* PKCS11_CONFIG_SECONDARY - secondary pkcs11.cfg 파일 내용 (미설정 시 이중화 비활성)
|
||||
* PIN_SECONDARY - secondary 슬롯 PIN (미설정 시 PIN 재사용)
|
||||
* RELOAD_INTERVAL_MINUTES - KeyStore 주기적 재로드 간격(분)
|
||||
*
|
||||
* PKCS11_CONFIG 값 예시 (개행은 \n 으로 입력):
|
||||
* name = ProtectServer
|
||||
* library = /opt/safenet/protecttoolkit5/ptk/lib/libcryptoki.so
|
||||
* slot = 0
|
||||
*
|
||||
* name = SoftHSM
|
||||
* library = C:/SoftHSM2/lib/softhsm2-x64.dll
|
||||
* slotListIndex = 0
|
||||
*
|
||||
* ---------------------------------------------------------------------
|
||||
* [설정 이중화 / primary 자동 복귀]
|
||||
* 기동(init) 과 재연결 시 항상 primary -> secondary 순서로 연결을 시도하고,
|
||||
* 최초로 성공한 설정을 사용한다. secondary 로 절체된 뒤에는 주기적 재로드마다
|
||||
* primary 재연결을 먼저 시도하므로 primary 가 복구되면 자동으로 되돌아온다.
|
||||
*
|
||||
* [세션 누적 방지 / 회로차단 로직]
|
||||
* 재로드마다 KeyStore.getInstance(...).load(null, pin) 을 새로 호출하면
|
||||
* PKCS11 세션(C_OpenSession)이 계속 누적되고, 결국 HSM 파티션의 최대 세션 수를
|
||||
* 초과하면서 reload 가 영구적으로 실패한다. 이를 방지하기 위해:
|
||||
* 1) primary 로 동작 중이고 설정도 그대로면 "기존 keyStore 인스턴스"에 다시
|
||||
* load() 하여 세션을 재사용한다 (= primary 우선 시도와 동일한 의미)
|
||||
* 2) 실제 키 조회(probe)로 세션이 살아있는지 검증
|
||||
* 3) 재로드가 실패하거나 프로퍼티가 변경되면 primary -> secondary 순서로
|
||||
* Provider 를 완전히 재생성
|
||||
* 4) 재생성마저 실패하면 마지막으로 성공한 keyStore 를 유지 (서비스 연속성 우선)
|
||||
*
|
||||
* [프로퍼티 즉시 반영]
|
||||
* PropManager 의 PropertyChangeListener 로 등록되어 있어, 관리 포털에서 HSM 그룹을
|
||||
* reload 하면 다음 스케줄을 기다리지 않고 즉시 설정을 다시 읽어 재연결을 시도한다.
|
||||
* 이 경우에도 신규 연결이 완전히 성공한 뒤에만 keyStore 멤버변수를 교체한다.
|
||||
* ---------------------------------------------------------------------
|
||||
*/
|
||||
@Component
|
||||
public class HsmManager implements Lifecycle, PropertyChangeListener {
|
||||
|
||||
static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
|
||||
|
||||
private static final String GROUP_NAME = "HSM";
|
||||
private static final String PROP_CONFIG = "PKCS11_CONFIG";
|
||||
private static final String PROP_PIN = "PIN";
|
||||
private static final String PROP_CONFIG_SECONDARY = "PKCS11_CONFIG_SECONDARY";
|
||||
private static final String PROP_PIN_SECONDARY = "PIN_SECONDARY";
|
||||
private static final String PROP_RELOAD_INTERVAL_MINUTES = "RELOAD_INTERVAL_MINUTES";
|
||||
|
||||
/** 설정 구분자. HSM 상태 조회 API 응답에도 그대로 노출된다. */
|
||||
public static final String CONFIG_PRIMARY = "PRIMARY";
|
||||
public static final String CONFIG_SECONDARY = "SECONDARY";
|
||||
|
||||
private static final long DEFAULT_RELOAD_INTERVAL_MINUTES = 10;
|
||||
|
||||
private Provider pkcs11Provider;
|
||||
private volatile KeyStore keyStore;
|
||||
private boolean started;
|
||||
|
||||
private final LifecycleSupport lifecycle = new LifecycleSupport(this);
|
||||
|
||||
private volatile char[] pin;
|
||||
|
||||
/** 현재 연결에 사용 중인 설정. 미연결이면 null. */
|
||||
private volatile HsmConfig activeConfig;
|
||||
|
||||
// ---- 모니터링용 상태값 (HsmStatusController 에서 조회) ----
|
||||
private volatile long activeSince;
|
||||
private volatile long lastReloadAt;
|
||||
private volatile String lastReloadResult;
|
||||
private volatile String lastErrorMessage;
|
||||
private volatile long lastErrorAt;
|
||||
private volatile int reloadSuccessCount;
|
||||
private volatile int reloadFailCount;
|
||||
private volatile int failoverCount;
|
||||
|
||||
private volatile long reloadIntervalMinutes = DEFAULT_RELOAD_INTERVAL_MINUTES;
|
||||
private ScheduledExecutorService scheduler;
|
||||
private ScheduledFuture<?> reloadFuture;
|
||||
private volatile boolean propListenerRegistered;
|
||||
|
||||
private HsmManager() {
|
||||
}
|
||||
|
||||
public static HsmManager getInstance() {
|
||||
return ApplicationContextProvider.getContext().getBean(HsmManager.class);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 설정 후보 (primary / secondary)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 한 번의 연결 시도에 필요한 설정 한 벌. PropManager 에서 읽어 복호화까지 마친 값이다.
|
||||
*/
|
||||
static final class HsmConfig {
|
||||
|
||||
final String name;
|
||||
final String configContent;
|
||||
final char[] pin;
|
||||
|
||||
HsmConfig(String name, String configContent, char[] pin) {
|
||||
this.name = name;
|
||||
this.configContent = configContent;
|
||||
this.pin = pin;
|
||||
}
|
||||
|
||||
/** PropManager 값이 바뀌었는지 판단하기 위한 비교. */
|
||||
boolean sameAs(HsmConfig other) {
|
||||
if (other == null) {
|
||||
return false;
|
||||
}
|
||||
return name.equals(other.name)
|
||||
&& configContent.equals(other.configContent)
|
||||
&& Arrays.equals(pin, other.pin);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 연결 시도 결과. Security 등록 전 상태이며, 완전히 성공한 경우에만
|
||||
* applyConnection() 을 통해 멤버변수로 승격된다.
|
||||
*/
|
||||
private static final class HsmConnection {
|
||||
|
||||
final HsmConfig config;
|
||||
final Provider provider;
|
||||
final KeyStore keyStore;
|
||||
|
||||
HsmConnection(HsmConfig config, Provider provider, KeyStore keyStore) {
|
||||
this.config = config;
|
||||
this.provider = provider;
|
||||
this.keyStore = keyStore;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PropManager 에서 매번 새로 읽어 primary -> secondary 순서의 연결 후보를 만든다.
|
||||
* 스케줄러/프로퍼티 변경 이벤트 모두 이 메서드를 통하므로 변경된 값이 즉시 반영된다.
|
||||
* PKCS11_CONFIG 가 비어 있는 후보는 제외한다.
|
||||
*/
|
||||
private List<HsmConfig> loadConfigCandidates() {
|
||||
PropManager propManager = PropManager.getInstance();
|
||||
EncryptionManager encManager = EncryptionManager.getInstance();
|
||||
|
||||
List<HsmConfig> candidates = new ArrayList<>();
|
||||
|
||||
String primaryConfig = decrypt(encManager, propManager.getProperty(GROUP_NAME, PROP_CONFIG));
|
||||
String primaryPin = decrypt(encManager, propManager.getProperty(GROUP_NAME, PROP_PIN));
|
||||
if (StringUtils.isNotBlank(primaryConfig)) {
|
||||
candidates.add(new HsmConfig(CONFIG_PRIMARY, normalizeConfig(primaryConfig), toPin(primaryPin)));
|
||||
}
|
||||
|
||||
String secondaryConfig = decrypt(encManager, propManager.getProperty(GROUP_NAME, PROP_CONFIG_SECONDARY));
|
||||
if (StringUtils.isNotBlank(secondaryConfig)) {
|
||||
// PIN_SECONDARY 미설정이면 primary PIN 을 재사용한다 (슬롯만 이중화하는 구성 지원)
|
||||
String rawSecondaryPin = propManager.getProperty(GROUP_NAME, PROP_PIN_SECONDARY);
|
||||
String secondaryPin = StringUtils.isNotBlank(rawSecondaryPin) ? decrypt(encManager, rawSecondaryPin) : primaryPin;
|
||||
candidates.add(new HsmConfig(CONFIG_SECONDARY, normalizeConfig(secondaryConfig), toPin(secondaryPin)));
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private static String decrypt(EncryptionManager encManager, String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
return encManager.decryptDBData(value);
|
||||
}
|
||||
|
||||
private static String normalizeConfig(String configContent) {
|
||||
return configContent.trim().replace("\\n", "\n");
|
||||
}
|
||||
|
||||
private static char[] toPin(String pinStr) {
|
||||
return (pinStr != null) ? pinStr.toCharArray() : null;
|
||||
}
|
||||
|
||||
private static HsmConfig findByName(List<HsmConfig> candidates, String name) {
|
||||
if (name == null) {
|
||||
return null;
|
||||
}
|
||||
for (HsmConfig candidate : candidates) {
|
||||
if (name.equals(candidate.name)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public void start() throws LifecycleException {
|
||||
if (started) {
|
||||
throw new LifecycleException("RECEAIHSM001");
|
||||
}
|
||||
lifecycle.fireLifecycleEvent(STARTING_EVENT, this);
|
||||
|
||||
try {
|
||||
init();
|
||||
startReloadScheduler();
|
||||
registerPropertyChangeListener();
|
||||
} catch (Exception e) {
|
||||
throw new LifecycleException(ExceptionUtil.getErrorCode(e, "RECEAIHSM002"));
|
||||
}
|
||||
|
||||
started = true;
|
||||
lifecycle.fireLifecycleEvent(STARTED_EVENT, this);
|
||||
}
|
||||
|
||||
private void init() throws Exception {
|
||||
|
||||
applyReloadInterval();
|
||||
|
||||
List<HsmConfig> candidates = loadConfigCandidates();
|
||||
if (candidates.isEmpty()) {
|
||||
logger.warn("HsmManager] PKCS11_CONFIG 가 설정되지 않았습니다. HSM 초기화를 건너뜁니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
HsmConnection connection = connectFirstAvailable(candidates);
|
||||
applyConnection(connection);
|
||||
|
||||
logger.warn("HsmManager] 초기화 완료. config=" + connection.config.name
|
||||
+ ", Provider=" + connection.provider.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* RELOAD_INTERVAL_MINUTES 를 다시 읽어 적용한다.
|
||||
*
|
||||
* @return 값이 변경되어 스케줄 재등록이 필요하면 true
|
||||
*/
|
||||
private boolean applyReloadInterval() {
|
||||
long newInterval = reloadIntervalMinutes;
|
||||
try {
|
||||
String value = PropManager.getInstance().getProperty(GROUP_NAME, PROP_RELOAD_INTERVAL_MINUTES,
|
||||
String.valueOf(DEFAULT_RELOAD_INTERVAL_MINUTES));
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
newInterval = Long.parseLong(value.trim());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("HsmManager] " + PROP_RELOAD_INTERVAL_MINUTES + " 값이 올바르지 않아 기존 값("
|
||||
+ reloadIntervalMinutes + "분)을 유지합니다: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (newInterval <= 0) {
|
||||
newInterval = DEFAULT_RELOAD_INTERVAL_MINUTES;
|
||||
}
|
||||
if (newInterval == reloadIntervalMinutes) {
|
||||
return false;
|
||||
}
|
||||
reloadIntervalMinutes = newInterval;
|
||||
return true;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 연결
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 후보 설정을 primary -> secondary 순서로 시도하여 최초로 성공한 연결을 반환한다.
|
||||
* 각 후보는 Provider 생성 + KeyStore.load + 키 목록 조회(probe)까지 모두 성공해야
|
||||
* 정상으로 간주한다. 전부 실패하면 마지막 예외를 던진다.
|
||||
*/
|
||||
private HsmConnection connectFirstAvailable(List<HsmConfig> candidates) throws Exception {
|
||||
Exception lastException = null;
|
||||
|
||||
for (HsmConfig config : candidates) {
|
||||
try {
|
||||
HsmConnection connection = connect(config);
|
||||
logger.warn("HsmManager] HSM 연결 성공. config=" + config.name
|
||||
+ ", Provider=" + connection.provider.getName());
|
||||
return connection;
|
||||
} catch (Exception e) {
|
||||
lastException = e;
|
||||
recordError(config.name + " 설정 연결 실패: " + e.getMessage());
|
||||
logger.warn("HsmManager] HSM 연결 실패. config=" + config.name + " : " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
if (lastException != null) {
|
||||
throw lastException;
|
||||
}
|
||||
throw new HsmException("HSM 연결 후보 설정이 없습니다.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 단일 설정으로 Provider/KeyStore 를 새로 생성한다. Security 에는 아직 등록하지 않으므로
|
||||
* 여기서 실패해도 현재 사용 중인 Provider/keyStore 는 영향을 받지 않는다.
|
||||
*/
|
||||
private HsmConnection connect(HsmConfig config) throws Exception {
|
||||
Provider provider = createProvider(config.configContent);
|
||||
KeyStore ks = KeyStore.getInstance("PKCS11", provider);
|
||||
ks.load(null, config.pin);
|
||||
logKeyStore(ks);
|
||||
return new HsmConnection(config, provider, ks);
|
||||
}
|
||||
|
||||
/**
|
||||
* 신규 연결을 멤버변수로 승격한다. 기존 Provider 는 이 시점에서만 logout/제거된다.
|
||||
*/
|
||||
private void applyConnection(HsmConnection connection) {
|
||||
Provider oldProvider = this.pkcs11Provider;
|
||||
HsmConfig oldConfig = this.activeConfig;
|
||||
|
||||
if (oldProvider != null && oldProvider != connection.provider) {
|
||||
if (oldProvider instanceof AuthProvider) {
|
||||
try {
|
||||
((AuthProvider) oldProvider).logout();
|
||||
} catch (Exception logoutEx) {
|
||||
logger.warn("HsmManager] 기존 세션 logout 실패(무시하고 진행): " + logoutEx.getMessage());
|
||||
}
|
||||
}
|
||||
Security.removeProvider(oldProvider.getName());
|
||||
}
|
||||
|
||||
// 동일 이름으로 이미 등록된 Provider 가 있으면 제거 후 재등록
|
||||
Provider registered = Security.getProvider(connection.provider.getName());
|
||||
if (registered != null && registered != connection.provider) {
|
||||
Security.removeProvider(registered.getName());
|
||||
}
|
||||
Security.addProvider(connection.provider);
|
||||
|
||||
this.pkcs11Provider = connection.provider;
|
||||
this.keyStore = connection.keyStore;
|
||||
this.activeConfig = connection.config;
|
||||
this.pin = connection.config.pin;
|
||||
this.activeSince = System.currentTimeMillis();
|
||||
|
||||
if (oldConfig != null && !oldConfig.name.equals(connection.config.name)) {
|
||||
failoverCount++;
|
||||
logger.warn("HsmManager] HSM 설정 절체: " + oldConfig.name + " -> " + connection.config.name);
|
||||
}
|
||||
}
|
||||
|
||||
private void logKeyStore(KeyStore keyStore)
|
||||
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
|
||||
java.util.Enumeration<String> aliases = keyStore.aliases();
|
||||
StringBuilder aliasList = new StringBuilder();
|
||||
while (aliases.hasMoreElements()) {
|
||||
if (aliasList.length() > 0) aliasList.append(", ");
|
||||
|
||||
String alias = aliases.nextElement();
|
||||
aliasList.append(alias);
|
||||
|
||||
java.security.Key key = keyStore.getKey(alias, null);
|
||||
if (key instanceof SecretKey) {
|
||||
SecretKey secretKey = (SecretKey) key;
|
||||
if (secretKey.getEncoded() == null) {
|
||||
logger.warn("HsmManager] HSM - secretKey null {} : [{}]", alias, secretKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.warn("HsmManager] HSM 키 목록: [" + aliasList + "]");
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 주기적 재로드
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 별도 스레드에서 주기적으로 KeyStore 를 다시 로드하여
|
||||
* HSM 에 새로 생성/추가된 키를 인식하도록 한다.
|
||||
*/
|
||||
private void startReloadScheduler() {
|
||||
scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "hsm-keystore-reloader");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
reloadFuture = scheduler.scheduleWithFixedDelay(
|
||||
this::reloadKeyStoreSafely,
|
||||
reloadIntervalMinutes,
|
||||
reloadIntervalMinutes,
|
||||
TimeUnit.MINUTES
|
||||
);
|
||||
|
||||
logger.warn("HsmManager] KeyStore 주기적 재로드 스케줄러 시작. interval=" + reloadIntervalMinutes + "분");
|
||||
}
|
||||
|
||||
/** RELOAD_INTERVAL_MINUTES 변경 시 스케줄을 다시 등록한다. */
|
||||
private synchronized void rescheduleReload() {
|
||||
if (scheduler == null || scheduler.isShutdown()) {
|
||||
return;
|
||||
}
|
||||
if (reloadFuture != null) {
|
||||
reloadFuture.cancel(false);
|
||||
}
|
||||
reloadFuture = scheduler.scheduleWithFixedDelay(
|
||||
this::reloadKeyStoreSafely,
|
||||
reloadIntervalMinutes,
|
||||
reloadIntervalMinutes,
|
||||
TimeUnit.MINUTES
|
||||
);
|
||||
logger.warn("HsmManager] KeyStore 재로드 주기 변경 적용: " + reloadIntervalMinutes + "분");
|
||||
}
|
||||
|
||||
/**
|
||||
* 스케줄러에서 호출되는 래퍼. 예외가 스케줄러 스레드를 죽이지 않도록 반드시 catch 한다.
|
||||
* (ScheduledExecutorService 는 task 에서 예외가 던져지면 이후 스케줄을 자동으로 중단시킨다)
|
||||
*/
|
||||
private void reloadKeyStoreSafely() {
|
||||
try {
|
||||
reloadKeyStoreIfNeeded();
|
||||
} catch (Throwable t) {
|
||||
logger.warn("HsmManager] KeyStore 주기적 재로드 실패: " + t.getMessage(), t);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* KeyStore 를 다시 로드한다. 외부(getSecretKey 등)에서 키 미스 발생 시
|
||||
* 즉시 재시도용으로 직접 호출할 수도 있다.
|
||||
*
|
||||
* 처리 순서:
|
||||
* 1) PropManager 에서 설정을 매번 새로 읽는다 (변경값 즉시 반영)
|
||||
* 2) 설정이 변경되었거나 아직 연결이 없으면 primary -> secondary 순으로 전체 재연결
|
||||
* 3) 현재 secondary 로 동작 중이면 primary 복귀를 먼저 시도
|
||||
* 4) 그 외에는 기존 keyStore 인스턴스에 다시 load() (PKCS11 세션 재사용).
|
||||
* 실패하면 primary -> secondary 순으로 전체 재연결
|
||||
*
|
||||
* 어느 경로든 신규 연결이 완전히 성공한 경우에만 keyStore 멤버변수를 교체한다.
|
||||
*/
|
||||
public synchronized void reloadKeyStoreIfNeeded() throws Exception {
|
||||
|
||||
List<HsmConfig> candidates = loadConfigCandidates();
|
||||
if (candidates.isEmpty()) {
|
||||
logger.warn("HsmManager] PKCS11_CONFIG 가 설정되지 않아 재로드를 건너뜁니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
HsmConfig active = this.activeConfig;
|
||||
HsmConfig currentCandidate = findByName(candidates, active == null ? null : active.name);
|
||||
|
||||
// 2) 미연결 상태이거나 현재 사용 중인 설정값 자체가 변경된 경우
|
||||
if (active == null || pkcs11Provider == null || currentCandidate == null
|
||||
|| !currentCandidate.sameAs(active)) {
|
||||
logger.warn("HsmManager] HSM 설정 변경 또는 미연결 상태 감지 → 전체 재연결을 수행합니다.");
|
||||
reconnect(candidates);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) secondary 로 동작 중이면 primary 복귀를 우선 시도
|
||||
HsmConfig preferred = candidates.get(0);
|
||||
if (!preferred.name.equals(active.name)) {
|
||||
try {
|
||||
HsmConnection connection = connect(preferred);
|
||||
applyConnection(connection);
|
||||
recordReloadSuccess(preferred.name + " 설정으로 복귀 성공");
|
||||
logger.warn("HsmManager] " + preferred.name + " 설정으로 복귀했습니다.");
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
logger.warn("HsmManager] " + preferred.name + " 복귀 시도 실패. 현재 " + active.name
|
||||
+ " 설정을 유지합니다: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 4) 현재 연결 유지 재로드 (세션 재사용)
|
||||
try {
|
||||
if (keyStore != null) {
|
||||
keyStore.load(null, pin);
|
||||
logKeyStore(keyStore);
|
||||
logger.warn("HsmManager] KeyStore 재로드 완료 (config=" + active.name + ", 기존 세션 재사용).");
|
||||
} else {
|
||||
KeyStore ks = KeyStore.getInstance("PKCS11", pkcs11Provider);
|
||||
ks.load(null, pin);
|
||||
logKeyStore(ks);
|
||||
this.keyStore = ks;
|
||||
logger.warn("HsmManager] KeyStore 신규 생성 완료 (config=" + active.name + ").");
|
||||
}
|
||||
recordReloadSuccess(active.name + " 설정 재로드 성공");
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.warn("HsmManager] KeyStore 재로드 실패:" + e.getMessage(), e);
|
||||
logger.warn("HsmManager] 후보 설정(PRIMARY→SECONDARY) 전체 재연결을 시도합니다.");
|
||||
reconnect(candidates);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 세션 누적, 네트워크 단절, 설정 변경 등으로 일반 재로드가 더 이상 복구되지 않을 때
|
||||
* primary -> secondary 순서로 Provider 를 완전히 새로 생성한다.
|
||||
*
|
||||
* 신규 Provider/KeyStore 준비가 완전히 성공한 후에만 기존 Provider 를 제거하고 교체한다.
|
||||
* 모든 후보가 실패하면 기존 Provider 와 keyStore 를 그대로 유지한다
|
||||
* (서비스 중단보다 마지막 정상 상태 보존을 우선).
|
||||
*/
|
||||
private void reconnect(List<HsmConfig> candidates) throws Exception {
|
||||
try {
|
||||
HsmConnection connection = connectFirstAvailable(candidates);
|
||||
applyConnection(connection);
|
||||
recordReloadSuccess(connection.config.name + " 설정 재연결 성공");
|
||||
logger.warn("HsmManager] HSM 재연결 성공. config=" + connection.config.name);
|
||||
} catch (Exception e) {
|
||||
recordReloadFailure(e.getMessage());
|
||||
logger.warn("HsmManager] HSM 재연결 실패. 이전 keyStore 를 그대로 유지합니다: " + e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private void recordReloadSuccess(String detail) {
|
||||
lastReloadAt = System.currentTimeMillis();
|
||||
lastReloadResult = "SUCCESS - " + detail;
|
||||
reloadSuccessCount++;
|
||||
}
|
||||
|
||||
private void recordReloadFailure(String detail) {
|
||||
lastReloadAt = System.currentTimeMillis();
|
||||
lastReloadResult = "FAIL - " + detail;
|
||||
reloadFailCount++;
|
||||
recordError(detail);
|
||||
}
|
||||
|
||||
private void recordError(String message) {
|
||||
lastErrorMessage = message;
|
||||
lastErrorAt = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// PropManager 변경 즉시 반영
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void registerPropertyChangeListener() {
|
||||
if (propListenerRegistered) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
PropManager.getInstance().addPropertyChangeListener(this);
|
||||
propListenerRegistered = true;
|
||||
logger.warn("HsmManager] PropManager PropertyChangeListener 등록 완료");
|
||||
} catch (Exception e) {
|
||||
logger.warn("HsmManager] PropManager PropertyChangeListener 등록 실패(주기적 재로드로 대체): "
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void unregisterPropertyChangeListener() {
|
||||
if (!propListenerRegistered) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
PropManager.getInstance().removePropertyChangeListener(this);
|
||||
} catch (Exception e) {
|
||||
logger.warn("HsmManager] PropManager PropertyChangeListener 해제 실패(무시): " + e.getMessage());
|
||||
}
|
||||
propListenerRegistered = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 관리 포털에서 PropManager.reload("HSM") 또는 setProperty 를 호출하면 수신된다.
|
||||
* 재로드 자체는 HSM 통신을 수반하므로 호출 스레드를 막지 않도록 스케줄러 스레드에 위임한다.
|
||||
*/
|
||||
@Override
|
||||
public void propertyChange(PropertyChangeEvent evt) {
|
||||
if (!isHsmGroupEvent(evt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (applyReloadInterval()) {
|
||||
rescheduleReload();
|
||||
}
|
||||
|
||||
ScheduledExecutorService currentScheduler = this.scheduler;
|
||||
if (currentScheduler == null || currentScheduler.isShutdown()) {
|
||||
return;
|
||||
}
|
||||
logger.warn("HsmManager] HSM 프로퍼티 변경 감지 → 설정 재적용을 요청합니다.");
|
||||
currentScheduler.execute(this::reloadKeyStoreSafely);
|
||||
}
|
||||
|
||||
/**
|
||||
* PropManager 는 reload(group) 시 propertyName 에 그룹명을, setProperty 시 키명을 담고
|
||||
* source 에는 항상 해당 그룹의 PropGroupVO 를 담는다. 두 경우를 모두 인식한다.
|
||||
*/
|
||||
private boolean isHsmGroupEvent(PropertyChangeEvent evt) {
|
||||
if (evt == null) {
|
||||
return false;
|
||||
}
|
||||
if (GROUP_NAME.equals(evt.getPropertyName())) {
|
||||
return true;
|
||||
}
|
||||
Object source = evt.getSource();
|
||||
return (source instanceof PropGroupVO) && GROUP_NAME.equals(((PropGroupVO) source).getName());
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Provider 생성
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* JDK 버전에 따라 SunPKCS11 Provider 를 생성한다.
|
||||
*
|
||||
* JDK 8 : SunPKCS11(InputStream) 생성자를 리플렉션으로 호출
|
||||
* JDK 9+: Provider.configure(configFilePath) 를 리플렉션으로 호출
|
||||
*
|
||||
* 두 경로 모두 리플렉션을 사용하므로 컴파일 타임에 JDK 버전 의존성이 없다.
|
||||
*/
|
||||
static Provider createProvider(String cfgContent) throws Exception {
|
||||
String javaVersion = System.getProperty("java.version");
|
||||
if (javaVersion.startsWith("1.")) {
|
||||
// JDK 8: new SunPKCS11(InputStream)
|
||||
InputStream is = new ByteArrayInputStream(cfgContent.getBytes("UTF-8"));
|
||||
return (Provider) Class.forName("sun.security.pkcs11.SunPKCS11")
|
||||
.getConstructor(InputStream.class)
|
||||
.newInstance(is);
|
||||
} else {
|
||||
// JDK 9+: Security.getProvider("SunPKCS11").configure(configFilePath)
|
||||
// configure() 는 JDK 9 에서 추가된 메서드이므로 리플렉션으로 호출
|
||||
File tmp = File.createTempFile("pkcs11-hsm-", ".cfg");
|
||||
tmp.deleteOnExit();
|
||||
Files.write(tmp.toPath(), cfgContent.getBytes("UTF-8"));
|
||||
Provider base = Security.getProvider("SunPKCS11");
|
||||
Method configure = Provider.class.getMethod("configure", String.class);
|
||||
return (Provider) configure.invoke(base, tmp.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws LifecycleException {
|
||||
if (!started) {
|
||||
throw new LifecycleException("RECEAIHSM003");
|
||||
}
|
||||
lifecycle.fireLifecycleEvent(STOPING_EVENT, this);
|
||||
|
||||
unregisterPropertyChangeListener();
|
||||
|
||||
if (reloadFuture != null) {
|
||||
reloadFuture.cancel(false);
|
||||
}
|
||||
if (scheduler != null) {
|
||||
scheduler.shutdown();
|
||||
}
|
||||
|
||||
if (pkcs11Provider != null) {
|
||||
if (pkcs11Provider instanceof AuthProvider) {
|
||||
try {
|
||||
((AuthProvider) pkcs11Provider).logout();
|
||||
} catch (Exception e) {
|
||||
logger.warn("HsmManager] 종료 시 logout 실패(무시): " + e.getMessage());
|
||||
}
|
||||
}
|
||||
Security.removeProvider(pkcs11Provider.getName());
|
||||
}
|
||||
pkcs11Provider = null;
|
||||
keyStore = null;
|
||||
activeConfig = null;
|
||||
activeSince = 0;
|
||||
started = false;
|
||||
|
||||
lifecycle.fireLifecycleEvent(STOPPED_EVENT, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addLifecycleListener(LifecycleListener listener) {
|
||||
lifecycle.addLifecycleListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LifecycleListener[] findLifecycleListeners() {
|
||||
return lifecycle.findLifecycleListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeLifecycleListener(LifecycleListener listener) {
|
||||
lifecycle.removeLifecycleListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStarted() {
|
||||
return started;
|
||||
}
|
||||
|
||||
public Provider getPkcs11Provider() {
|
||||
return pkcs11Provider;
|
||||
}
|
||||
|
||||
public KeyStore getKeyStore() {
|
||||
return keyStore;
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
return started && pkcs11Provider != null && keyStore != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* isReady() 와 달리 실제 HSM 호출로 세션 생존 여부까지 확인하는 헬스체크.
|
||||
* 모니터링/헬스체크 엔드포인트에서 사용을 권장한다.
|
||||
*/
|
||||
public boolean isHealthy() {
|
||||
if (!isReady()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
logKeyStore(keyStore);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 모니터링용 조회 (HsmStatusController)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/** 현재 연결에 사용 중인 설정 이름(PRIMARY/SECONDARY). 미연결이면 null. */
|
||||
public String getActiveConfigName() {
|
||||
HsmConfig config = this.activeConfig;
|
||||
return (config == null) ? null : config.name;
|
||||
}
|
||||
|
||||
/** 현재 secondary 설정으로 절체된 상태인지 여부. */
|
||||
public boolean isUsingSecondaryConfig() {
|
||||
return CONFIG_SECONDARY.equals(getActiveConfigName());
|
||||
}
|
||||
|
||||
/** 현재 연결에 실제로 사용된 pkcs11.cfg 내용. PIN 은 포함되지 않는다. */
|
||||
public String getActiveConfigContent() {
|
||||
HsmConfig config = this.activeConfig;
|
||||
return (config == null) ? null : config.configContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 PropManager 값 기준으로 실제 적용될 pkcs11.cfg 내용을 후보별로 반환한다.
|
||||
* PIN 은 포함하지 않는다. (설정이름 -> cfg 내용, primary 우선순)
|
||||
*/
|
||||
public java.util.Map<String, String> getResolvedConfigContents() {
|
||||
java.util.Map<String, String> contents = new java.util.LinkedHashMap<>();
|
||||
try {
|
||||
for (HsmConfig config : loadConfigCandidates()) {
|
||||
contents.put(config.name, config.configContent);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("HsmManager] 연결 후보 설정 조회 실패: " + e.getMessage());
|
||||
}
|
||||
return contents;
|
||||
}
|
||||
|
||||
/** 현재 KeyStore 의 alias 목록. HSM 통신이 발생한다. */
|
||||
public List<String> getKeyAliases() throws Exception {
|
||||
KeyStore ks = this.keyStore;
|
||||
if (ks == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<String> aliases = new ArrayList<>();
|
||||
java.util.Enumeration<String> e = ks.aliases();
|
||||
while (e.hasMoreElements()) {
|
||||
aliases.add(e.nextElement());
|
||||
}
|
||||
return aliases;
|
||||
}
|
||||
|
||||
public String getProviderName() {
|
||||
Provider provider = this.pkcs11Provider;
|
||||
return (provider == null) ? null : provider.getName();
|
||||
}
|
||||
|
||||
public long getReloadIntervalMinutes() {
|
||||
return reloadIntervalMinutes;
|
||||
}
|
||||
|
||||
/** 현재 설정으로 연결된 시각(epoch millis). 미연결이면 0. */
|
||||
public long getActiveSince() {
|
||||
return activeSince;
|
||||
}
|
||||
|
||||
public long getLastReloadAt() {
|
||||
return lastReloadAt;
|
||||
}
|
||||
|
||||
public String getLastReloadResult() {
|
||||
return lastReloadResult;
|
||||
}
|
||||
|
||||
public String getLastErrorMessage() {
|
||||
return lastErrorMessage;
|
||||
}
|
||||
|
||||
public long getLastErrorAt() {
|
||||
return lastErrorAt;
|
||||
}
|
||||
|
||||
public int getReloadSuccessCount() {
|
||||
return reloadSuccessCount;
|
||||
}
|
||||
|
||||
public int getReloadFailCount() {
|
||||
return reloadFailCount;
|
||||
}
|
||||
|
||||
/** primary <-> secondary 절체가 발생한 횟수. */
|
||||
public int getFailoverCount() {
|
||||
return failoverCount;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user