diff --git a/WebContent/WEB-INF/web.xml b/WebContent/WEB-INF/web.xml index 0b2219d..1265260 100644 --- a/WebContent/WEB-INF/web.xml +++ b/WebContent/WEB-INF/web.xml @@ -22,6 +22,17 @@ /mgr/* + + ApiRequestBodyFilter + com.eactive.eai.adapter.controller.ApiRequestBodyFilter + + + ApiRequestBodyFilter + /api/* + /mapi/* + + + logbackDisableServletContainerInitializer true diff --git a/docs/API_TRANSACTION_FLOW.md b/docs/API_TRANSACTION_FLOW.md new file mode 100644 index 0000000..6605510 --- /dev/null +++ b/docs/API_TRANSACTION_FLOW.md @@ -0,0 +1,500 @@ +# API 거래 플로우 정리 + +## 전체 플로우 다이어그램 + +``` +[Client Request] + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ 1. ApiAdapterController (/api/**, /mapi/**) │ +│ - HTTP 요청 수신, 어댑터 정보 조회 │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ 2. ApiAdapterService │ +│ - 요청 파싱 (GET/POST/PUT/DELETE) │ +│ - 암/복호화 처리 (AES128, AES256 등) │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ 3. HttpAdapterServiceSupport.service() │ +│ - Pre/Post Filter 실행 │ +│ - RequestDispatcher 호출 │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ 4. RequestDispatcher.handle() │ +│ - Processor 인스턴스 생성/획득 │ +│ - Adapter 상태 관리 │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ 5. RequestProcessor.hookup() │ +│ - EAIMessage 생성 (StandardMessage → EAIMessage) │ +│ - 유량제어, 라우팅 판단 │ +│ - FlowRouter.process() 호출 │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ 6. IFRouter.process() │ +│ - Outbound 라우팅 │ +│ - FailOver 처리 │ +│ - ElinkESBProcessProxy 호출 │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ 7. RESTProcess (extends DefaultProcess) │ +│ - flow() 메소드 실행 │ +│ - 송신 변환, 로그, 응답 처리 │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ 8. HttpSender.callService() │ +│ - ElinkAdapter 획득 │ +│ - Adapter 호출 │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────────────────────┐ +│ 9. HttpClient5AdapterServiceRest.execute() │ +│ - 실제 HTTP 통신 (Apache HttpClient 5) │ +│ - OAuth 토큰 관리 │ +│ - 암/복호화 처리 │ +└─────────────────────────────────────────────────────────────────┘ + ↓ +[Target System] +``` + +--- + +## 각 클래스 설명 + +### 1. ApiAdapterController + +| 항목 | 내용 | +|------|------| +| 위치 | `eapim-online/src/main/java/com/eactive/eai/adapter/controller/` | +| 역할 | REST API 진입점 (Spring Controller) | +| 주요 기능 | - `/api/**`, `/mapi/**` 엔드포인트 매핑
- TSEAIHS04 테이블에서 어댑터 정보 조회
- HttpDynamicInAdapterManager로 어댑터 찾기
- ApiAdapterService.callApi() 호출 | + +### 2. ApiAdapterService + +| 항목 | 내용 | +|------|------| +| 위치 | `eapim-online/src/main/java/com/eactive/eai/adapter/service/` | +| 역할 | Inbound 서비스 레이어 | +| 상속 | `HttpAdapterServiceSupport` | +| 주요 기능 | - HTTP Method별 요청 파싱
- Inbound 암/복호화 (AES128, AES256 등)
- 부모 클래스 `service()` 호출 | + +### 3. HttpAdapterServiceSupport + +| 항목 | 내용 | +|------|------| +| 위치 | `elink-online-common/src/main/java/com/eactive/eai/adapter/http/dynamic/` | +| 역할 | Inbound 어댑터 추상 클래스 | +| 주요 기능 | - `service()` 메소드: 핵심 디스패처
- Pre-Filter / Post-Filter 실행
- StandardMessage 생성
- RequestDispatcher.handle() 호출 | + +### 4. RequestDispatcher + +| 항목 | 내용 | +|------|------| +| 위치 | `elink-online-common/src/main/java/com/eactive/eai/adapter/` | +| 역할 | 요청 분배자 | +| 주요 기능 | - Processor 인스턴스 생성/획득
- `processor.execute()` 호출
- AdapterGroup 상태 관리 (동시 처리 수 증감) | + +### 5. RequestProcessorSupport / RequestProcessor + +| 항목 | 내용 | +|------|------| +| 위치 | `elink-online-common/src/main/java/com/eactive/eai/inbound/processor/` | +| 역할 | 요청 처리 프로세서 | +| 관계 | `RequestProcessor extends RequestProcessorSupport` | +| 주요 기능 | - `execute()` → `hookup()` 호출
- StandardMessage → EAIMessage 변환
- 유량제어 (RateLimiter)
- SYNC/ASYNC 분기 처리
- FlowRouter.process() 호출 | + +### 6. IFRouter + +| 항목 | 내용 | +|------|------| +| 위치 | `elink-online-common/src/main/java/com/eactive/eai/common/routing/` | +| 역할 | FlowControl → Outbound 라우터 | +| 주요 기능 | - Outbound Process로 라우팅
- Local/Remote 호출 판단
- FailOver 처리
- TAS(시뮬레이터) 거래 처리
- ElinkESBProcessProxy 호출 | + +### 7. DefaultProcess / RESTProcess + +| 항목 | 내용 | +|------|------| +| 위치 | `elink-online-common/src/main/java/com/eactive/eai/outbound/` | +| 역할 | Outbound 처리 프로세스 | +| 관계 | `RESTProcess extends HTTPProcess extends DefaultProcess` | +| 주요 기능 (DefaultProcess) | - `flow()`: 핵심 처리 흐름
- 송신 전/후 처리
- 메시지 변환 (Transform)
- 로그 처리 | +| 주요 기능 (RESTProcess) | - `outboundCtrlCallService()`: HttpSender 호출
- REST 특화 처리
- 표준/비표준 메시지 처리 | + +### 8. HttpSender + +| 항목 | 내용 | +|------|------| +| 위치 | `elink-online-common/src/main/java/com/eactive/eai/control/` | +| 역할 | HTTP 송신 제어 | +| 주요 기능 | - ElinkAdapterFactory에서 HTTP Adapter 획득
- `adapter.callService()` 호출
- SocketTimeout, HttpStatusException 처리 | + +### 9. HttpClient5AdapterServiceRest + +| 항목 | 내용 | +|------|------| +| 위치 | `elink-online-common/src/main/java/com/eactive/eai/adapter/http/client/impl/` | +| 역할 | 실제 HTTP 클라이언트 구현체 | +| 주요 기능 | - Apache HttpClient 5 사용
- GET/POST/PUT/DELETE 메소드 처리
- OAuth2 토큰 관리 (발급/갱신)
- Outbound 암/복호화 (AES128, AES256, AES256GCM 등)
- Forward Proxy 지원
- 응답 헤더 릴레이 | + +--- + +## 클래스 관계도 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ eapim-online (프로젝트) │ +├─────────────────────────────────────────────────────────────────────────┤ +│ ApiAdapterController ──uses──▶ ApiAdapterService │ +│ │ │ +│ │ extends │ +│ ▼ │ +└──────────────────────────────────────┼──────────────────────────────────┘ + │ +┌──────────────────────────────────────┼──────────────────────────────────┐ +│ elink-online-common (서브모듈) │ +├──────────────────────────────────────┼──────────────────────────────────┤ +│ ▼ │ +│ HttpAdapterServiceSupport │ +│ │ │ +│ │ calls │ +│ ▼ │ +│ RequestDispatcher ──creates──▶ RequestProcessor │ +│ │ │ +│ │ extends │ +│ ▼ │ +│ RequestProcessorSupport │ +│ │ │ +│ │ calls │ +│ ▼ │ +│ FlowRouter ────────────────▶ IFRouter │ +│ │ │ +│ │ calls │ +│ ▼ │ +│ RESTProcess ◀──extends── HTTPProcess │ +│ │ │ │ +│ │ extends │ extends │ +│ ▼ ▼ │ +│ DefaultProcess ◀────────────┘ │ +│ │ │ +│ │ calls │ +│ ▼ │ +│ HttpSender │ +│ │ │ +│ │ uses │ +│ ▼ │ +│ ElinkAdapterFactory │ +│ │ │ +│ │ creates │ +│ ▼ │ +│ HttpClient5AdapterServiceRest │ +│ │ │ +│ │ uses │ +│ ▼ │ +│ Apache HttpClient 5 │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 메시지 변환 흐름 + +``` +[HTTP Request Body] + ↓ + StandardMessage (표준전문) + ↓ + EAIMessage (내부 전문) + ↓ + Transform (변환) + ↓ + [Target System Request] + ↓ + [Target System Response] + ↓ + Transform (역변환) + ↓ + EAIMessage + ↓ + StandardMessage + ↓ + [HTTP Response Body] +``` + +--- + +## 주요 처리 패턴 + +| 패턴 | 설명 | 처리 위치 | +|------|------|-----------| +| **SS (Sync-Sync)** | 동기 요청 → 동기 응답 | RequestProcessor, RESTProcess | +| **SA (Sync-Async)** | 동기 요청 → 비동기 응답 (Topic 대기) | RESTProcess.getTopicProperty() | +| **AS (Async-Sync)** | 비동기 요청 → 동기 응답 | DefaultProcess.isAsyncSyncPattern() | +| **AA (Async-Async)** | 비동기 요청 → 비동기 응답 | RequestProcessor | + +--- + +## 비동기(Async) 처리 플로우 + +### 비동기 전체 흐름 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ [송신 스레드 - 요청 수신] │ +│ │ +│ RequestProcessor.hookup() │ +│ ├─ ROUTING_TYPE = "QUEUE" 판단 │ +│ │ │ +│ └─ FlowRouter.process() │ +│ └─ JMSSender.sendToQueue(eaiMessage, ...) │ +│ └─ EAIMessage 직렬화 → JMS Queue 전송 │ +│ │ │ +│ ← 즉시 응답 반환 (202 Accepted 등) │ +└─────────────────────────┼───────────────────────────────────────────────┘ + │ + [JMS Queue] + (ActiveMQ / WebLogic JMS / WebSphere MQ) + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ [수신 스레드 - QueueConsumer] │ +│ │ +│ DefaultQueueConsumer.run() │ +│ ├─ consumer.receive() - 메시지 수신 대기 │ +│ │ │ +│ ├─ EAIMessage 역직렬화 │ +│ │ └─ transactionContext Map 복원 │ +│ │ │ +│ ├─ ElinkTransactionContext.initialize(...) │ +│ │ │ +│ └─ ElinkESBProcessProxy.callElinkESBProcess() │ +│ └─ DefaultProcess / RESTProcess 호출 │ +│ └─ 실제 대외기관 호출 │ +│ │ +│ (finally) ElinkTransactionContext.clear() │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Queue 송신 (FlowRouter → JMSSender) + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ FlowRouter.process() │ +│ │ │ +│ ├─ route_type == "QUEUE" 확인 │ +│ │ │ +│ ├─ RoutingVO에서 asyncRoutingPath(Queue 이름) 조회 │ +│ │ │ +│ ├─ Properties에 추가 정보 설정 │ +│ │ ├─ EAIBWKCLS (업무그룹코드) │ +│ │ ├─ EAISVCCD (서비스코드) │ +│ │ └─ OSIDINSTCD (대외기관코드) │ +│ │ │ +│ └─ JMSSender.sendToQueue(eaiMessage, factory, queue, prop) │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ JMSSender.sendToQueue() │ +│ │ │ +│ ├─ ElinkConfig.isUseInternalQueue() 확인 │ +│ │ │ +│ ├─ [내부 Queue] AmqQueueProducer.sendMessage() │ +│ │ └─ ActiveMQ 내장 브로커 사용 (vm://localhost) │ +│ │ │ +│ └─ [외부 Queue] sendToJmsQueue() │ +│ ├─ JmsServiceLocator에서 QueueConnectionFactory 획득 │ +│ ├─ QueueConnection → QueueSession → QueueSender 생성 │ +│ ├─ ObjectMessage 생성 │ +│ │ ├─ msg.setObject(eaiMessage) ← Serializable │ +│ │ └─ msg.setStringProperty(...) ← Properties 복사 │ +│ └─ qsender.send(msg) │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### Queue Consumer 수신 및 처리 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ QueueConsumerService (Spring Bean) │ +│ │ │ +│ ├─ afterPropertiesSet() → startConsumer() │ +│ │ │ +│ ├─ ActiveMQConnectionFactory 생성 │ +│ │ └─ setTrustAllPackages(true) ← 역직렬화 허용 │ +│ │ │ +│ ├─ ExecutorService (FixedThreadPool) 생성 │ +│ │ │ +│ └─ DefaultQueueConsumer[maxThread] 생성 및 submit │ +│ └─ 각 Consumer가 별도 스레드에서 실행 │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ Consumer.run() [Loop] (DefaultQueueConsumer / FCQueueReceiver) │ +│ │ │ +│ ├─ while(run) { │ +│ │ message = consumer.receive(2000) ← 2초 타임아웃 │ +│ │ │ +│ │ if(message == null) continue; │ +│ │ │ +│ │ // ObjectMessage 검증 및 EAIMessage 역직렬화 │ +│ │ omsg = (ObjectMessage) message; │ +│ │ eaiMessage = (EAIMessage) omsg.getObject(); │ +│ │ │ +│ │ // RoutingVO에서 Process 경로 조회 │ +│ │ routingVO = RoutingManager.getRoutingVO(flwCntlRtnNm); │ +│ │ serviceURI = routingVO.getSyncRoutingPath(); // BaseFCProcess │ +│ │ │ +│ │ // Process 호출 (ElinkESBProcessProxy 내부에서 컨텍스트 관리) │ +│ │ ElinkESBProcessProxy.callElinkESBProcess(serviceURI, ...) │ +│ │ } │ +└─────────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ ElinkESBProcessProxy.callElinkESBProcess() │ +│ │ │ +│ ├─ [조건 충족: ASYNC && !outbound] → 컨텍스트 초기화 │ +│ │ ElinkTransactionContext.initialize(eaiMessage.getTransactionContext()) +│ │ │ +│ ├─ BaseFCProcess.callService() │ +│ │ └─ ElinkESBProcessProxy (RESTProcess) ← 조건 미충족, 초기화 안함 │ +│ │ └─ RESTProcess 실행 │ +│ │ │ +│ └─ finally { ElinkTransactionContext.clear() } ← 조건부 해제 │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 비동기 관련 클래스 + +| 클래스 | 위치 | 역할 | +|--------|------|------| +| `FlowRouter` | `elink-online-common/.../routing/` | ROUTING_TYPE에 따라 CALL/QUEUE/TOPIC 분기 | +| `JMSSender` | `elink-online-common/.../util/` | JMS Queue/Topic 메시지 송신 | +| `AmqQueueProducer` | `elink-online-common/.../activemq/` | ActiveMQ 내장 브로커용 Producer | + +#### JMS Provider별 Consumer 구현체 + +| Provider | Consumer | 비고 | +|----------|----------|------| +| **ActiveMQ** | `flowcontrol/activemq/DefaultQueueConsumer.java` | 내장 브로커 (vm://localhost) | +| **JEUS** | `flowcontrol/jeus/DefaultQueueConsumer.java` | JNDI Lookup | +| **WebSphere** | `flowcontrol/websphere/DefaultQueueConsumer.java` | JNDI Lookup | +| **WebLogic** | `flowcontrol/jms/FCQueueReceiver.java` | JNDI Lookup (weblogic.jndi.WLInitialContextFactory) | + +> `ElinkTransactionContext` 초기화/해제는 Consumer가 아닌 `ElinkESBProcessProxy`에서 조건부로 처리됩니다. + +### Queue 연결 방식 + +| 방식 | 설정 | 사용 환경 | +|------|------|-----------| +| **내장 브로커** | `vm://localhost` | 단일 인스턴스, 테스트 환경 | +| **TCP 연결** | `tcp://host:61617` | 분산 환경, 다중 인스턴스 | +| **외부 JMS** | JNDI Lookup | WebLogic, WebSphere MQ 등 | + +### 환경별 Consumer 설정 (Spring XML) + +``` +WebContent/WEB-INF/ +├─ applicationContext.xml ← 메인 설정 +│ ├─ import: mq/applicationContext-activemq.xml +│ └─ import: mq/applicationContext-websphere.xml +│ +└─ mq/ + ├─ applicationContext-activemq.xml ← ActiveMQ (현재 활성화) + │ └─ flowcontrol.activemq.QueueConsumerService + │ └─ uri: vm://localhost (내장 브로커) + │ + └─ applicationContext-websphere.xml ← WebSphere MQ (주석처리 = 비활성화) + └─ flowcontrol.websphere.QueueConsumerService + └─ 외부 WebSphere MQ 연결 +``` + +> **Note**: WebLogic/JEUS 환경에서 외부 JMS 사용 시 해당 XML 파일 추가 또는 수정 필요. +> 현재 프로젝트는 **ActiveMQ 내장 브로커**를 기본으로 사용. + +### TransactionContext 비동기 전달 + +``` +[송신 스레드] [수신 스레드] + │ │ + ├─ eaiMsg.transactionContext │ + │ └─ {"SEED_SALT": "abc123", ...} │ + │ │ + ▼ │ +┌──────────────┐ │ +│ Serialize │ ──── JMS Queue ────▶ │ +│ (EAIMessage) │ │ +└──────────────┘ │ + ▼ + ┌──────────────────┐ + │ Deserialize │ + │ (EAIMessage) │ + │ └─ context 복원 │ + └──────────────────┘ + │ + ▼ + ElinkESBProcessProxy + └─ 조건부 initialize() + │ + ElinkTransactionContext + .getSeedSalt() → "abc123" +``` + +> **Note**: `EAIMessage.transactionContext`는 `Serializable`이므로 JMS Queue를 통해 전달되어도 데이터가 유지됩니다. +> `ElinkTransactionContext.initialize()`는 `ElinkESBProcessProxy`에서 ASYNC 거래 && !outbound 조건일 때만 호출됩니다. + +--- + +## 암호화 지원 (HttpClient5AdapterServiceRest) + +| 알고리즘 | 용도 | Key 추출 방식 | +|----------|------|---------------| +| AES128-TOSS | 토스 연동 | clientSecret[22:38] | +| AES128-TOGETHER | 투게더 연동 | clientSecret[44:60] | +| AES256 | 일반 | clientSecret[10:42] | +| AES256-KAKAO | 카카오 연동 | IV/Key 별도 설정 | +| AES256-NICEON | 나이스지키미 연동 | Adapter Token 기반 | +| AES256GCM | GCM 모드 | 동적 Key 생성 | + +--- + +## 관련 파일 경로 + +### eapim-online (메인 프로젝트) +- `src/main/java/com/eactive/eai/adapter/controller/ApiAdapterController.java` +- `src/main/java/com/eactive/eai/adapter/service/ApiAdapterService.java` + +### elink-online-common (서브모듈) + +#### 동기 처리 +- `src/main/java/com/eactive/eai/adapter/http/dynamic/HttpAdapterServiceSupport.java` +- `src/main/java/com/eactive/eai/adapter/RequestDispatcher.java` +- `src/main/java/com/eactive/eai/inbound/processor/RequestProcessorSupport.java` +- `src/main/java/com/eactive/eai/inbound/processor/RequestProcessor.java` +- `src/main/java/com/eactive/eai/common/routing/IFRouter.java` +- `src/main/java/com/eactive/eai/outbound/DefaultProcess.java` +- `src/main/java/com/eactive/eai/outbound/RESTProcess.java` +- `src/main/java/com/eactive/eai/control/HttpSender.java` +- `src/main/java/com/eactive/eai/adapter/http/client/impl/HttpClient5AdapterServiceRest.java` + +#### 비동기 처리 (Queue) +- `src/main/java/com/eactive/eai/common/routing/FlowRouter.java` - 라우팅 분기 (CALL/QUEUE/TOPIC) +- `src/main/java/com/eactive/eai/common/routing/ElinkESBProcessProxy.java` - Process 호출 (비동기 컨텍스트 관리) +- `src/main/java/com/eactive/eai/common/util/JMSSender.java` - Queue/Topic 송신 +- `src/main/java/com/eactive/eai/common/activemq/AmqQueueProducer.java` - ActiveMQ Producer +- `src/main/java/com/eactive/eai/flowcontrol/activemq/QueueConsumerService.java` - ActiveMQ Consumer 서비스 +- `src/main/java/com/eactive/eai/flowcontrol/activemq/DefaultQueueConsumer.java` - ActiveMQ Consumer +- `src/main/java/com/eactive/eai/flowcontrol/jeus/QueueConsumerService.java` - JEUS Consumer 서비스 +- `src/main/java/com/eactive/eai/flowcontrol/jeus/DefaultQueueConsumer.java` - JEUS Consumer +- `src/main/java/com/eactive/eai/flowcontrol/websphere/QueueConsumerService.java` - WebSphere Consumer 서비스 +- `src/main/java/com/eactive/eai/flowcontrol/websphere/DefaultQueueConsumer.java` - WebSphere Consumer +- `src/main/java/com/eactive/eai/flowcontrol/jms/FCQueueReceiver.java` - WebLogic JMS Consumer diff --git a/docs/ELINK_TRANSACTION_CONTEXT.md b/docs/ELINK_TRANSACTION_CONTEXT.md new file mode 100644 index 0000000..12bb0a7 --- /dev/null +++ b/docs/ELINK_TRANSACTION_CONTEXT.md @@ -0,0 +1,300 @@ +# ElinkTransactionContext 아키텍처 + +## 개요 + +`ElinkTransactionContext`는 트랜잭션(거래) 범위에서 컨텍스트 변수를 관리하기 위한 ThreadLocal 기반 유틸리티입니다. JEP 함수 등 EAIMessage나 Properties에 직접 접근할 수 없는 클래스에서 트랜잭션 범위 변수에 접근할 수 있도록 합니다. + +--- + +## 핵심 설계 원칙 + +1. **ThreadLocal이 직접 저장소**: EAIMessage의 Map을 참조하지 않고 ThreadLocal에 직접 저장 +2. **Lazy Initialization**: 최초 put 시 Map 자동 생성 (명시적 초기화 불필요) +3. **clear()는 필수**: 스레드 풀 환경에서 메모리 누수 방지 +4. **비동기 전달용 필드 분리**: `transactionContextTransfer`는 JMS 전달 전용 + +--- + +## 구성 요소 + +### 1. ElinkTransactionContext (저장소) + +```java +// ElinkTransactionContext.java (elink-online-core) +public class ElinkTransactionContext { + private static final ThreadLocal> THREAD_LOCAL = new ThreadLocal<>(); + + // 컨텍스트 정리 (필수) + public static void clear(); + + // 비동기 전달용 + public static Map getAll(); // 복사본 반환 + public static void restore(Map ctx); // 복원 + + // 명시적 접근자 (Lazy init - 최초 put 시 Map 생성) + public static void setSeedSalt(String seedSalt); + public static String getSeedSalt(); + public static void setEncryptionKey(String encryptionKey); + public static String getEncryptionKey(); +} +``` + +- **위치**: `elink-online-core` +- **역할**: ThreadLocal 기반 트랜잭션 변수 저장소 +- **특징**: + - 명시적 초기화 불필요 (Lazy init) + - `clear()`는 필수 (finally 블록에서 호출) + +### 2. EAIMessage.transactionContextTransfer (전달용) + +```java +// EAIMessage.java (elink-online-common) +/** + * [주의] 비동기 JMS 전달 전용 - 직접 사용 금지! + */ +private Map transactionContextTransfer; + +public Map getTransactionContextTransfer(); +public void setTransactionContextTransfer(Map ctx); +``` + +- **위치**: `elink-online-common` +- **역할**: 비동기 거래 시 JMS를 통한 컨텍스트 전달 +- **특징**: + - `Serializable` - ObjectMessage로 직렬화 + - **직접 데이터 저장 용도로 사용 금지** + - 복원 후 즉시 null로 초기화 + +--- + +## 처리 흐름 + +### 동기 (Sync) 처리 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ ApiAdapterController.callApi() │ +│ │ │ +│ ├─ try { │ +│ │ service.callApi() │ +│ │ └─ RequestProcessor.hookup() │ +│ │ │ │ +│ │ ├─ [컨텍스트 사용 가능 - Lazy Init] ───────────────── │ +│ │ │ ElinkTransactionContext.setSeedSalt("value") │ +│ │ │ └─ 최초 호출 시 Map 자동 생성 │ +│ │ │ │ +│ │ ├─ FlowRouter.process() │ +│ │ │ └─ ElinkESBProcessProxy → Process 호출 │ +│ │ │ │ +│ │ ├─ [반환 후에도 컨텍스트 사용 가능] ✓ │ +│ │ │ │ +│ │ └─ 응답 반환 │ +│ │ } │ +│ └─ finally { │ +│ ElinkTransactionContext.clear() ← 최상위 진입점에서 해제 │ +│ } │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +### 비동기 (Async) 처리 - Queue 사용 + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ [송신 스레드] │ +│ │ +│ ApiAdapterController.callApi() │ +│ │ │ +│ ├─ try { │ +│ │ RequestProcessor.hookup() │ +│ │ ├─ ElinkTransactionContext.setSeedSalt("value") │ +│ │ │ └─ ThreadLocal에 직접 저장 │ +│ │ │ │ +│ │ └─ FlowRouter.process() │ +│ │ ├─ [컨텍스트 복사] ───────────────────────────────────── │ +│ │ │ eaiMessage.setTransactionContextTransfer( │ +│ │ │ ElinkTransactionContext.getAll() │ +│ │ │ ) │ +│ │ └─ JMSSender.sendToQueue(eaiMessage, ...) │ +│ │ └─ EAIMessage 직렬화 (transactionContextTransfer 포함)│ +│ │ } │ +│ └─ finally { ElinkTransactionContext.clear() } │ +│ │ │ +└──────────────────────────┼──────────────────────────────────────────────┘ + │ + [JMS Queue] + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────┐ +│ [수신 스레드 - Consumer] │ +│ │ +│ FCQueueReceiver / DefaultQueueConsumer │ +│ │ │ +│ ├─ EAIMessage 역직렬화 (transactionContextTransfer 복원) │ +│ │ │ +│ └─ ElinkESBProcessProxy.callElinkESBProcess(BaseFCProcess, ...) │ +│ │ │ +│ ├─ [조건 충족: ASYNC && !outbound] │ +│ │ ├─ ElinkTransactionContext.restore( │ +│ │ │ eaiMessage.getTransactionContextTransfer() │ +│ │ │ ) │ +│ │ └─ eaiMessage.setTransactionContextTransfer(null) ← 즉시 초기화 +│ │ │ +│ ├─ BaseFCProcess.callService() │ +│ │ └─ ElinkESBProcessProxy(..., RESTProcess, ...) │ +│ │ └─ [조건 미충족: outbound] → 복원 안함 │ +│ │ └─ RESTProcess 호출 │ +│ │ └─ ElinkTransactionContext.getSeedSalt() │ +│ │ └─ 송신 스레드에서 설정한 값 조회 ✓ │ +│ │ │ +│ └─ finally { ElinkTransactionContext.clear() } │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 초기화/해제 위치 + +| 위치 | 역할 | 비고 | +|------|------|------| +| `ApiAdapterController.callApi()` | 해제 (finally) | 동기 - 최상위 진입점 | +| `RequestProcessorSupport.execute()` | 해제 (finally) | 동기 - 안전망 (이중 해제) | +| `FlowRouter.process()` | 컨텍스트 복사 | 비동기 송신 전 | +| `ElinkESBProcessProxy.callElinkESBProcess()` | 복원/해제 (조건부) | 비동기 수신 시 | + +### ElinkESBProcessProxy 복원 조건 + +```java +boolean shouldManageContext = eaiMessage != null + && EAIMessageKeys.ASYNC_SVC.equals(eaiMessage.getSvcTsmtUsgTp()) // ASYNC 거래 + && (serviceURI == null || !serviceURI.startsWith("com.eactive.eai.outbound")); // outbound가 아닐 때 + +if (shouldManageContext) { + ElinkTransactionContext.restore(eaiMessage.getTransactionContextTransfer()); + eaiMessage.setTransactionContextTransfer(null); // 복원 후 즉시 초기화 +} +``` + +- **ASYNC 거래**: 동기 거래는 Lazy init으로 자동 처리 +- **outbound가 아닐 때**: BaseFCProcess 호출 시에만 복원, RESTProcess 호출 시에는 이미 복원된 상태 + +> **Note**: `clear()`는 멱등(idempotent)하므로 여러 번 호출되어도 안전합니다. + +--- + +## 사용 예시 + +### 값 설정 (RequestProcessor, DefaultProcess 등) + +```java +// SeedSalt 설정 (최초 호출 시 Map 자동 생성) +ElinkTransactionContext.setSeedSalt("o123y498172"); + +// EncryptionKey 설정 +ElinkTransactionContext.setEncryptionKey("myKey123"); +``` + +### 값 조회 (JEP 함수, Transform 등) + +```java +// SeedSalt 조회 +String seedSalt = ElinkTransactionContext.getSeedSalt(); + +// EncryptionKey 조회 +String encryptionKey = ElinkTransactionContext.getEncryptionKey(); +``` + +--- + +## 변수 추가 방법 + +새로운 트랜잭션 변수가 필요한 경우 `ElinkTransactionContext`에 접근자 메소드 추가: + +```java +// ElinkTransactionContext.java + +// 1. Key 상수 추가 +private static final String KEY_NEW_VARIABLE = "NEW_VARIABLE"; + +// 2. Setter 추가 (private put 헬퍼 사용) +public static void setNewVariable(String value) { + put(KEY_NEW_VARIABLE, value); +} + +// 3. Getter 추가 (private get 헬퍼 사용) +public static String getNewVariable() { + return get(KEY_NEW_VARIABLE); +} +``` + +--- + +## 주의사항 + +### Thread Pool 환경 + +- `ApiAdapterController.callApi()` finally에서 `clear()` 호출 +- `ElinkESBProcessProxy.callElinkESBProcess()` finally에서 조건부 `clear()` 호출 +- Thread 재사용 시 이전 트랜잭션 데이터 누수 방지 + +### 비동기 Queue 처리 + +- `EAIMessage.transactionContextTransfer`는 **전달 전용** (직접 접근 금지) +- `FlowRouter`에서 송신 전 `getAll()`로 복사 +- `ElinkESBProcessProxy`에서 `restore()` 후 즉시 null 초기화 + +### transactionContextTransfer 직접 사용 금지 + +```java +// ❌ 잘못된 사용 +eaiMessage.getTransactionContextTransfer().put("key", "value"); + +// ✓ 올바른 사용 +ElinkTransactionContext.setSeedSalt("value"); +``` + +--- + +## 관련 파일 + +| 파일 | 위치 | 역할 | +|------|------|------| +| `ElinkTransactionContext.java` | `elink-online-core` | ThreadLocal 컨텍스트 저장소 | +| `EAIMessage.java` | `elink-online-common` | transactionContextTransfer (전달용) | +| `ApiAdapterController.java` | `eapim-online` | 해제 (동기 최상위 진입점) | +| `FlowRouter.java` | `elink-online-common` | 비동기 송신 전 컨텍스트 복사 | +| `ElinkESBProcessProxy.java` | `elink-online-common` | 복원/해제 (비동기 - 조건부) | +| `RequestProcessorSupport.java` | `elink-online-common` | 해제 (동기 안전망) | + +--- + +## 클래스 다이어그램 + +``` +┌─────────────────────────────────────┐ +│ elink-online-core │ +├─────────────────────────────────────┤ +│ ElinkTransactionContext │ +│ ┌───────────────────────────────┐ │ +│ │ ThreadLocal>│◄── 직접 저장소 (Lazy Init) +│ └───────────────────────────────┘ │ +│ + clear() │ +│ + getAll() → Map 복사본 │ +│ + restore(Map) │ +│ + setSeedSalt() / getSeedSalt() │ +│ + setEncryptionKey() / ... │ +└───────────────┬─────────────────────┘ + │ 비동기 전달 시 + │ getAll() → 복사 + ▼ +┌─────────────────────────────────────┐ +│ elink-online-common │ +├─────────────────────────────────────┤ +│ EAIMessage │ +│ - transactionContextTransfer │◄── JMS 전달 전용 (직접 사용 금지!) +│ : Map │ Serializable +│ │ +│ [송신] FlowRouter에서 설정 │ +│ [수신] ElinkESBProcessProxy에서 │ +│ restore() 후 null 초기화 │ +└─────────────────────────────────────┘ +``` diff --git a/docs/사용자정의함수.md b/docs/사용자정의함수.md new file mode 100644 index 0000000..7656ed2 --- /dev/null +++ b/docs/사용자정의함수.md @@ -0,0 +1,70 @@ +# 사용자 정의 변환 함수 + +## 1. substringrequestmessage + +최초요청데이터를 substring + +| 항목 | 값 | +|------|-----| +| 변환함수명 | substringrequestmessage | +| 변환함수반환유형ID | [2101] STRING | +| 변환함수유형ID | [2302] GENERIC | +| 변환함수생성클래스 | com.eactive.eai.custom.transformer.function.userdefined.SubStringRequestMessage | + +### 파라미터 + +| 일련번호 | 파라미터명 | 유형 | 설명 | +|---------|-----------|------|------| +| 1 | start | NUMBER | 시작 위치 | +| 2 | end | NUMBER | 종료 위치 | + +### 사용 예시 + +``` +substringrequestmessage(AGW_TESTCASE019S1_TGTR.status,3,7) +``` + +**샘플 메시지:** +``` +ABCDEFGHIJK +``` + +**예상 결과:** +``` +DEFG +``` + +--- + +## 2. jsonpathextractrequestmessage + +최초요청(100)이 JSON 일 경우 특정 PATH의 데이터를 추출 하는 함수 + +| 항목 | 값 | +|------|-----| +| 변환함수명 | jsonpathextractrequestmessage | +| 변환함수반환유형ID | [2116] OBJECT | +| 변환함수유형ID | [2302] GENERIC | +| 변환함수생성클래스 | com.eactive.eai.custom.transformer.function.userdefined.JsonPathExtractRequestMessage | + +### 파라미터 + +| 일련번호 | 파라미터명 | 유형 | 설명 | +|---------|-----------|------|------| +| 1 | jsonPath | STRING | 대상 PATH | + +### 사용 예시 + +``` +jsonpathextractrequestmessage(AGW_TESTCASE022S1_TGTR.transactionId,"$.group1.field2") +``` + +**샘플 메시지:** +```json +{"group1":{"field1":"value1","field2":"value2"},"field3":"value3"} +``` + +**예상 결과:** +``` +value2 +``` diff --git a/elink-online-common b/elink-online-common index 8d21eba..51db6c8 160000 --- a/elink-online-common +++ b/elink-online-common @@ -1 +1 @@ -Subproject commit 8d21eba5e8dc25d11ad315aa4559566cd3623bba +Subproject commit 51db6c88cfc2d827f90141ceeef0d798b7d2abf3 diff --git a/elink-online-core b/elink-online-core index 0f148e9..75e3d34 160000 --- a/elink-online-core +++ b/elink-online-core @@ -1 +1 @@ -Subproject commit 0f148e997ec808e01a013baa8ca7b0053a69fe22 +Subproject commit 75e3d3451c0965ad7bbf88a469211afb75f3dc5f diff --git a/elink-online-core-jpa b/elink-online-core-jpa index 2cd9c29..ea932d1 160000 --- a/elink-online-core-jpa +++ b/elink-online-core-jpa @@ -1 +1 @@ -Subproject commit 2cd9c29d4df228c1ffd7ed6471c8245510b19213 +Subproject commit ea932d1fddecd1f1fa8fc52959bcc6cfb455434a diff --git a/elink-online-emsclient b/elink-online-emsclient index f68423b..7cd000f 160000 --- a/elink-online-emsclient +++ b/elink-online-emsclient @@ -1 +1 @@ -Subproject commit f68423b40ecb5076990739f09f87ec62f7fd17ae +Subproject commit 7cd000f6927095a87d622c921ffe944a3802f3d7 diff --git a/elink-online-transformer b/elink-online-transformer index c3e2de4..dd68924 160000 --- a/elink-online-transformer +++ b/elink-online-transformer @@ -1 +1 @@ -Subproject commit c3e2de482be0fa692a5133cf77e0e3dc86357b6e +Subproject commit dd68924d78c70c0baf37970dd658e7accc72e260 diff --git a/libs/security-1.0.0.jar b/libs/security-1.0.0.jar new file mode 100644 index 0000000..2e46994 Binary files /dev/null and b/libs/security-1.0.0.jar differ diff --git a/src/main/java/com/eactive/eai/adapter/controller/ApiAdapterController.java b/src/main/java/com/eactive/eai/adapter/controller/ApiAdapterController.java index 11feef8..213cc3a 100644 --- a/src/main/java/com/eactive/eai/adapter/controller/ApiAdapterController.java +++ b/src/main/java/com/eactive/eai/adapter/controller/ApiAdapterController.java @@ -60,7 +60,7 @@ public class ApiAdapterController implements HttpAdapterServiceKey { String apiUri = servletRequest.getRequestURI(); // /api/v1/public/getUserInfo.svc apiUri = StringUtils.removeStart(apiUri, servletRequest.getContextPath()); - apiUri = StringUtils.removeEnd(apiUri, "/"); + apiUri = StringUtils.removeEnd(apiUri, "/"); //** jwhong TSEAIHS04의 api full path와 비교하여 adapter를 가져온다 String methodAndUri = servletRequest.getMethod() + "|" + apiUri; diff --git a/src/main/java/com/eactive/eai/adapter/controller/ApiRequestBodyFilter.java b/src/main/java/com/eactive/eai/adapter/controller/ApiRequestBodyFilter.java new file mode 100644 index 0000000..4e13781 --- /dev/null +++ b/src/main/java/com/eactive/eai/adapter/controller/ApiRequestBodyFilter.java @@ -0,0 +1,85 @@ +package com.eactive.eai.adapter.controller; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.charset.Charset; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + + +public class ApiRequestBodyFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) + throws ServletException, IOException { + + // 1. 필터 대상 + if (!isTarget(request)) { + filterChain.doFilter(request, response); + return; + } + + // 2. 요청 전문 읽기 + String body = readBody(request); + if (body == null || body.isEmpty()) { + filterChain.doFilter(request, response); + return; + } + + String modifiedBody = body; + + try { + Object json = JSONValue.parse(body); + + // 3. 예제: ROOTLESS ARRAY 보정 + if (json instanceof JSONArray) { + JSONObject wrap = new JSONObject(); + wrap.put("KJB_ROOTLESS_ARRAY", json); + modifiedBody = wrap.toJSONString(); + } + + } catch (Exception e) { + // 변조 실패 → 원본 유지 + modifiedBody = body; + } + + byte[] bytes = modifiedBody.getBytes(request.getCharacterEncoding() != null ? request.getCharacterEncoding() : Charset.defaultCharset().name() ); + + // 5. Wrapper로 재주입 + HttpServletRequest wrapped = + new CachedBodyHttpServletRequest(request, bytes); + + filterChain.doFilter(wrapped, response); + } + + private boolean isTarget(HttpServletRequest request) { + String uri = request.getRequestURI(); + + + return ("POST".equals(request.getMethod()) || "PUT".equals(request.getMethod())) + && (uri.startsWith("/api") || uri.startsWith("/mapi") ) && !uri.startsWith("/mapi/oauth2/token"); + } + + private String readBody(HttpServletRequest request) throws IOException { + StringBuilder sb = new StringBuilder(); + try (BufferedReader br = request.getReader()) { + String line; + while ((line = br.readLine()) != null) { + sb.append(line); + } + } + return sb.toString(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/eactive/eai/adapter/controller/ApiResponseAdvice.java b/src/main/java/com/eactive/eai/adapter/controller/ApiResponseAdvice.java new file mode 100644 index 0000000..4827f88 --- /dev/null +++ b/src/main/java/com/eactive/eai/adapter/controller/ApiResponseAdvice.java @@ -0,0 +1,52 @@ +package com.eactive.eai.adapter.controller; + +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +@RestControllerAdvice +public class ApiResponseAdvice implements ResponseBodyAdvice { + + @Override + public boolean supports(MethodParameter returnType, Class> converterType) { + // TODO Auto-generated method stub + return true; + } + + @Override + public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, + Class> selectedConverterType, ServerHttpRequest request, + ServerHttpResponse response) { + + if (body instanceof String) { + + return removeRootlessArrayKeyFormResponseBody((String) body); + } + + return body; + } + + String removeRootlessArrayKeyFormResponseBody(String body) { + try { + Object root = JSONValue.parse(body); + + if (root instanceof JSONObject && ((JSONObject) root).containsKey("KJB_ROOTLESS_ARRAY")) { + JSONArray jsonArray = (JSONArray) ((JSONObject) root).get("KJB_ROOTLESS_ARRAY"); + return jsonArray.toJSONString(); + } else { + return body; + } + } catch (Exception e) { + return body; + } + + } + +} \ No newline at end of file diff --git a/src/main/java/com/eactive/eai/adapter/controller/CachedBodyHttpServletRequest.java b/src/main/java/com/eactive/eai/adapter/controller/CachedBodyHttpServletRequest.java new file mode 100644 index 0000000..c3c0f9e --- /dev/null +++ b/src/main/java/com/eactive/eai/adapter/controller/CachedBodyHttpServletRequest.java @@ -0,0 +1,51 @@ +package com.eactive.eai.adapter.controller; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.InputStreamReader; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; + +public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper { + + private final byte[] cachedBody; + + public CachedBodyHttpServletRequest(HttpServletRequest request, byte[] body) { + super(request); + this.cachedBody = body; + } + + @Override + public ServletInputStream getInputStream() { + ByteArrayInputStream bais = new ByteArrayInputStream(cachedBody); + + return new ServletInputStream() { + @Override + public int read() { + return bais.read(); + } + + @Override + public boolean isFinished() { + return bais.available() == 0; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + } + }; + } + + @Override + public BufferedReader getReader() { + return new BufferedReader(new InputStreamReader(getInputStream())); + } + } diff --git a/src/main/java/com/eactive/eai/adapter/service/ApiAdapterService.java b/src/main/java/com/eactive/eai/adapter/service/ApiAdapterService.java index 1f7be85..14acfa9 100644 --- a/src/main/java/com/eactive/eai/adapter/service/ApiAdapterService.java +++ b/src/main/java/com/eactive/eai/adapter/service/ApiAdapterService.java @@ -51,6 +51,7 @@ import java.io.IOException; import java.io.ObjectInputStream; import org.springframework.security.oauth2.provider.ClientDetails; import com.nimbusds.jwt.SignedJWT; +import com.eactive.eai.common.context.ElinkTransactionContext; @Service @@ -68,6 +69,7 @@ public class ApiAdapterService extends HttpAdapterServiceSupport { private static final String FILE_GROUP_NAME = "image-file"; private static final String UPLOAD_ROOT_PATH = "UPLOAD_ROOT_PATH"; public static final String PAYLOAD_PARAM_NAME_CLIENT_ID = "client_id"; // jwhong + private boolean encryptResponseApply; // inbound에 대한 응답 암호화 여부 flag public String callApi(HttpServletRequest request, HttpServletResponse response, Properties httpProp, AdapterGroupVO adapterGroupVO, AdapterVO adapterVO, Properties transactionProp) throws Exception { int traceLevel = 0; @@ -124,8 +126,11 @@ public class ApiAdapterService extends HttpAdapterServiceSupport { transactionProp.put(INBOUND_TOKEN, inboundToken); transactionProp.put(ENCRYPT_AES256_IV, httpProp.getProperty(ENCRYPT_AES256_IV, "")); //jwhong transactionProp.put(ENCRYPT_AES256_KEY, httpProp.getProperty(ENCRYPT_AES256_KEY, "")); //jwhong + + // SEED 컬럼암호하 시 Key로 사용함 + String seedkey = getHeaders(request).getOrDefault("x-obp-partnercode", "").toString(); + ElinkTransactionContext.setSeedKey(seedkey); - try { traceLevel = Integer.parseInt(traceLevelTemp); } catch (Exception e) { @@ -317,29 +322,10 @@ public class ApiAdapterService extends HttpAdapterServiceSupport { result = mapper.writeValueAsString(rootNode); } } - - // KJBank는 요청에 대한 응답 (Sync응답, Aync 에 대한 Ack응답) 에 대하여 암호화 하지 않는다. 무조건 안한다. 따라서 아래부분은 구현은 했지만 사용하지 않는다. - //result = doPostEncryption(result, transactionProp); // jwhong Encrypt - // ASYNC 인 경우 광주은행은 RETURN 해 주는 값들이 수정되어야 한다. JWHONG - /* - if ("ASYN".equals(syncAsyncType) && MessageType.JSON.equals(adptMsgType) && StringUtils.isNotBlank(headerGroupName)) { - ObjectMapper mapper = new ObjectMapper(); - ObjectNode rootNode = (ObjectNode) mapper.readTree(result); - JsonNode headerGroup = rootNode.get(headerGroupName); - if(headerGroup != null) { - for(Iterator it = headerGroup.fieldNames(); it.hasNext();) { - String name = it.next(); - String value = headerGroup.get(name).asText(); - response.addHeader(name, value); - } - rootNode.remove(headerGroupName); - result = mapper.writeValueAsString(rootNode); - } - } - */ - - // KJBank는 요청에 대한 응답 (Sync응답, Aync 에 대한 Ack응답) 에 대하여 암호화 하지 않는다. 무조건 안한다. 따라서 아래부분은 구현은 했지만 사용하지 않는다. - //result = doPostEncryption(result, transactionProp); // jwhong Encrypt + + // KJBank는 요청에 대한 응답 (Sync응답, Aync 에 대한 Ack응답) 에 대하여 암호화 하지 않는다. 무조건 안한다. 따라서 아래부분은 구현은 했지만 사용하지 않는다. + encryptResponseApply = StringUtils.equalsIgnoreCase(httpProp.getProperty("ENCRYPT_RESPONSE_APPLY", "N"), "Y"); + result = doPostEncryption(result, transactionProp, request); // jwhong Encrypt return result; } @@ -382,7 +368,7 @@ public class ApiAdapterService extends HttpAdapterServiceSupport { decryptedMessage = doInboundPreDecrypt(eaiBody, decryptAlgorithm,clientSecret,secretAES256Iv, secretAES256Key ); if (decryptedMessage == null) { - throw new Exception("[HttpClient5AdapterServiceRest] Decrypt Error : Invalid encrypted message"); + throw new Exception("[ApiAdapterService] Decrypt Error : Invalid encrypted message"); } @@ -393,19 +379,26 @@ public class ApiAdapterService extends HttpAdapterServiceSupport { // jwhong encrypt - private String doPostEncryption(String eaiBody, Properties transactionProp) { + private String doPostEncryption(String eaiBody, Properties transactionProp, HttpServletRequest request) throws Exception { + + if ( !encryptResponseApply) { + return eaiBody; + } + String decryptAlgorithm = transactionProp.getProperty(ENCRYPT_ALGORITHM, ""); String xElinkClientId = transactionProp.getProperty(HEADER_NAME_CLIENT_ID, ""); // 이 값이 OAuth의 client id 값이다 . http header 에 포함되어 온다. jwhong String encryptBaseKeyType = transactionProp.getProperty(ENCRYPT_BASE_TYPE); - String inboundToken = transactionProp.getProperty(INBOUND_TOKEN, ""); + String inboundToken = ""; String secretAES256Iv = transactionProp.getProperty("ENCRYPT_AES256_IV"); String secretAES256Key = transactionProp.getProperty("ENCRYPT_AES256_KEY"); - - + if ( decryptAlgorithm == "" ) { // 복호화 대상이 아님 return eaiBody; } + + inboundToken = JwtTokenExtractor(request); + xElinkClientId = JwtClientIdExtractor(inboundToken); // token에서 client id를 추출함 // 암호화 key를 token 내용으로 할지 client secret 내용으로 할지 결정함. adapter property에 정의함 // ENCRYPT_ALGORITHM 을 설정했는데 ENCRYPT_BASE_TYPE을 설정안하면 Decrypt 수행시 Exception 발생함 @@ -421,12 +414,16 @@ public class ApiAdapterService extends HttpAdapterServiceSupport { } } - byte[] decryptedMessage = null; - decryptedMessage = doInboundPostEncrypt(eaiBody, decryptAlgorithm,clientSecret,secretAES256Iv, secretAES256Key ); + String encryptedMessage = null; + encryptedMessage = doInboundPostEncrypt(eaiBody, decryptAlgorithm,clientSecret,secretAES256Iv, secretAES256Key ); - String resultMessage = new String(decryptedMessage, StandardCharsets.UTF_8 ); - return resultMessage; - //return new String(decryptedMessage, StandardCharsets.UTF_8 ); + if (encryptedMessage == null) { + throw new Exception("[ApiAdapterService] Encrypt Error : Invalid PlainText message"); + } + + return encryptedMessage; + //String resultMessage = new String(encryptedMessage, StandardCharsets.UTF_8 ); + //return resultMessage; } @@ -466,7 +463,7 @@ public class ApiAdapterService extends HttpAdapterServiceSupport { eaiStringBody = extractBodyMsg(decryptAlgorithm, eaiStringBody); if (eaiStringBody == null) { - throw new Exception("[HttpClient5AdapterServiceRest] Cannot decrypt Error : input is plain text"); + throw new Exception("[ApiAdapterService] Cannot decrypt Error : input is plain text"); } //test source @@ -568,28 +565,7 @@ public class ApiAdapterService extends HttpAdapterServiceSupport { String decryptedJsonKey = ""; String decryptedBodyMessage = ""; - switch (decryptAlgorithm) { - case "AES128": - case "AES128-TOSS": - decryptedJsonKey = "preScreeningRequest"; - break; - case "AES128-TOGETHER": - decryptedJsonKey = "obpTxData"; - break; - case "AES256": - decryptedJsonKey = "preScreeningRequest"; - break; - case "AES256-KAKAO": - decryptedJsonKey = "encrypted_data"; - break; - case "AES256-TOSS": - decryptedJsonKey = "encryptedData"; - break; - case "AES256GCM": - break; - default: - break; - } + decryptedJsonKey = getJsonKey(decryptAlgorithm); try { JSONParser parser = new JSONParser(); @@ -603,74 +579,95 @@ public class ApiAdapterService extends HttpAdapterServiceSupport { return decryptedBodyMessage; } - - private byte[] doInboundPostEncrypt(String eaiStringBody, String encryptAlgorithm, String clientSecretKey, String secretAES256Iv, String secretAES256Key) { - - byte[] encryptedMessage = null; - //String eaiStringBody = new String(eaiBody, StandardCharsets.UTF_8); - - //test source - logger.debug("Base64 문자열: " + eaiStringBody); - logger.debug("Base64 문자열 길이: " + eaiStringBody.length()); - logger.debug("Base64 문자열 끝: " + eaiStringBody.substring(eaiStringBody.length() - 10)); + + private String getJsonKey(String Algorithm) { - // Base64 디코딩 테스트 - try { - byte[] decoded = Base64.getDecoder().decode(eaiStringBody); - logger.debug("디코딩 성공, 길이: " + decoded.length); - } catch (IllegalArgumentException e) { - logger.debug("Base64 디코딩 실패: " + e.getMessage()); - } - // end test source + String jsonKey = ""; + + switch (Algorithm) { + case "AES128": + case "AES128-TOSS": + jsonKey = "preScreeningRequest"; + break; + case "AES128-TOGETHER": + jsonKey = "obpTxData"; + break; + case "AES256": + jsonKey = "preScreeningRequest"; + break; + case "AES256-KAKAO": + jsonKey = "encrypted_data"; + break; + case "AES256-TOSS": + jsonKey = "encryptedData"; + break; + case "AES256-NICEON": + jsonKey = "enc_data"; + break; + case "AES256GCM": + break; + default: + break; + } + return jsonKey; + } + private String doInboundPostEncrypt(String eaiStringBody, String encryptAlgorithm, String clientSecretKey, String secretAES256Iv, String secretAES256Key) { + + //byte[] encryptedMessage = null; + String encryptedStrMessage = null; + String encryptedJsonKey = ""; + + encryptedJsonKey = getJsonKey(encryptAlgorithm); + switch (encryptAlgorithm) { case "AES128": case "AES128-TOSS": try { String key128 = clientSecretKey.substring(22,38); //togetherEncoder 경우는 substring(44,60) 이네?? - String decryptedStrMessage = AESCipher.encrypt(eaiStringBody, key128); - encryptedMessage = decryptedStrMessage.getBytes(StandardCharsets.UTF_8); + encryptedStrMessage = AESCipher.encrypt(eaiStringBody, key128); + //encryptedMessage = encryptedStrMessage.getBytes(StandardCharsets.UTF_8); } catch (Exception e) { e.printStackTrace(); - logger.error("HttpClientAdapterServiceRest] AES128 Decryption error=" + e.getMessage()); + logger.error("HttpClientAdapterServiceRest] AES128 Encryption error=" + e.getMessage()); } break; case "AES128-TOGETHER": try { String key128 = clientSecretKey.substring(44,60); //togetherEncoder 경우는 substring(44,60) 이네?? - String decryptedStrMessage = AESCipher.encrypt(eaiStringBody, key128); - encryptedMessage = decryptedStrMessage.getBytes(StandardCharsets.UTF_8); + encryptedStrMessage = AESCipher.encrypt(eaiStringBody, key128); + //encryptedMessage = encryptedStrMessage.getBytes(StandardCharsets.UTF_8); } catch (Exception e) { e.printStackTrace(); - logger.error("HttpClientAdapterServiceRest] AES128 Decryption error=" + e.getMessage()); + logger.error("HttpClientAdapterServiceRest] AES128 Encryption error=" + e.getMessage()); } break; case "AES256": try { String key = clientSecretKey.substring(10,42); - String decryptedStrMessage = AES256Cipher.encrypt(eaiStringBody, key); - encryptedMessage = decryptedStrMessage.getBytes(StandardCharsets.UTF_8); + encryptedStrMessage = AES256Cipher.encrypt(eaiStringBody, key); + //encryptedMessage = encryptedStrMessage.getBytes(StandardCharsets.UTF_8); } catch (Exception e) { e.printStackTrace(); - logger.error("HttpClientAdapterServiceRest] AES256 Decryption error=" + e.getMessage()); + logger.error("HttpClientAdapterServiceRest] AES256 Encryption error=" + e.getMessage()); } break; case "AES256-KAKAO": try { - String decryptedStrMessage = AES256Cipher.encrypt(eaiStringBody, secretAES256Iv, secretAES256Key); - encryptedMessage = decryptedStrMessage.getBytes(StandardCharsets.UTF_8); + encryptedStrMessage = AES256Cipher.encrypt(eaiStringBody, secretAES256Iv, secretAES256Key); + //encryptedMessage = encryptedStrMessage.getBytes(StandardCharsets.UTF_8); } catch (Exception e) { e.printStackTrace(); - logger.error("HttpClientAdapterServiceRest] AES256 Decryption error=" + e.getMessage()); + logger.error("HttpClientAdapterServiceRest] AES256 Encryption error=" + e.getMessage()); } break; case "AES256-TOSS": try { - String decryptedStrMessage = AES256Cipher.encrypt(eaiStringBody, secretAES256Iv, secretAES256Key); - encryptedMessage = decryptedStrMessage.getBytes(StandardCharsets.UTF_8); + encryptedStrMessage = AES256Cipher.encrypt(eaiStringBody, secretAES256Iv, secretAES256Key); + //encryptedMessage = encryptedStrMessage.getBytes(StandardCharsets.UTF_8); } catch (Exception e) { e.printStackTrace(); - logger.error("HttpClientAdapterServiceRest] AES256 Decryption error=" + e.getMessage()); + logger.error("HttpClientAdapterServiceRest] AES256 Encryption error=" + e.getMessage()); } break; @@ -680,8 +677,8 @@ public class ApiAdapterService extends HttpAdapterServiceSupport { AES256GCMCipher cipher = new AES256GCMCipher(key); byte[] aad = "metadata".getBytes(); - String decryptedStrMessage = cipher.encrypt(eaiStringBody , aad); - encryptedMessage = decryptedStrMessage.getBytes(StandardCharsets.UTF_8); + encryptedStrMessage = cipher.encrypt(eaiStringBody , aad); + //encryptedMessage = encryptedStrMessage.getBytes(StandardCharsets.UTF_8); } catch (Exception e) { e.printStackTrace(); @@ -690,7 +687,8 @@ public class ApiAdapterService extends HttpAdapterServiceSupport { break; default: try { - encryptedMessage = eaiStringBody.getBytes(); + encryptedStrMessage = eaiStringBody; + //encryptedMessage = eaiStringBody.getBytes(); } catch (Exception e) { e.printStackTrace(); logger.error("HttpClientAdapterServiceRest] doOutboundPostFilter Array copy error=" + e.getMessage()); @@ -698,7 +696,12 @@ public class ApiAdapterService extends HttpAdapterServiceSupport { break; } - return encryptedMessage; + Map map = new HashMap<>(); + map.put(encryptedJsonKey, encryptedStrMessage); + JSONObject json = new JSONObject(); + json.putAll(map); + return json.toJSONString(); + //return encryptedMessage; } diff --git a/src/main/java/com/eactive/eai/authserver/custom/BearerTokenContoller.java b/src/main/java/com/eactive/eai/authserver/custom/BearerTokenContoller.java index d3d547f..44e32de 100644 --- a/src/main/java/com/eactive/eai/authserver/custom/BearerTokenContoller.java +++ b/src/main/java/com/eactive/eai/authserver/custom/BearerTokenContoller.java @@ -19,6 +19,7 @@ import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException; +import com.eactive.eai.authserver.service.BearerTokenService; import com.eactive.eai.authserver.service.OAuth2Manager; import com.eactive.eai.common.util.Logger; import com.eactive.eai.common.util.MessageUtil; @@ -85,22 +86,6 @@ public class BearerTokenContoller { } } -// // 서비스용 Bearer Token 발급 -// @PostMapping("/service") -// public ResponseEntity issueFileToken(@RequestBody Map req) { -// String fileId = req.get("fileid"); -// -// String token = tokenService.generateFileToken(fileId); -// -// Map tokenMap = new HashMap<>(); -// tokenMap.put("token_type", "Bearer"); -// tokenMap.put("file_token", token); -// tokenMap.put("expires_in", "599"); -// tokenMap.put("expires_on", "1575593514"); -// tokenMap.put("resource", fileId); -// -// return ResponseEntity.ok(tokenMap); -// } private void veryfyClient(ClientDetails clientDetails, String clientSecret, Set scopeSet) throws JwtAuthException { if (!StringUtils.equals(clientDetails.getClientSecret(), clientSecret)) { throw new JwtAuthException("invalid_client", "Bad client credentials(client_secret is empty or not match)"); diff --git a/src/main/java/com/eactive/eai/authserver/custom/BearerTokenFilter.java b/src/main/java/com/eactive/eai/authserver/custom/BearerTokenFilter.java deleted file mode 100644 index 5ccb70c..0000000 --- a/src/main/java/com/eactive/eai/authserver/custom/BearerTokenFilter.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.eactive.eai.authserver.custom; - -import java.io.IOException; -import java.util.Arrays; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.json.simple.JSONObject; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import com.eactive.eai.adapter.http.dynamic.filter.JwtAuthException; -import com.eactive.eai.common.util.Logger; -import com.eactive.eai.common.util.MessageUtil; - -@Component -public class BearerTokenFilter extends OncePerRequestFilter{ - - public static Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT); - private final BearerTokenService tokenService; - - public static final String ERROR_AUTHENTICATION_FAIL = "E.AUTHENTICATION_FAIL"; - public static final String ERROR_AUTHORIZATION_FAIL = "E.AUTHORIZATION_FAIL"; - public static final String ERROR_TOKEN_EXPIRED = "E.TOKEN_EXPIRED"; - - public BearerTokenFilter(BearerTokenService tokenService) { - this.tokenService = tokenService; - } - - @Override - protected void doFilterInternal(HttpServletRequest request, - HttpServletResponse response, - FilterChain filterChain) throws IOException, ServletException { - try { - String auth = request.getHeader("Authorization"); - - if (auth != null && auth.startsWith("Bearer ")) { - String token = auth.substring(7); - - String clientId = null; - String fileId = null; - - if (tokenService.validateCA(token)) { - clientId = tokenService.getClientIdFromCA(token); - } else { - throw new JwtAuthException(ERROR_AUTHENTICATION_FAIL, "Access token is invalid or expired"); - } - -// if (fileId != null) { -// UsernamePasswordAuthenticationToken authentication = -// new UsernamePasswordAuthenticationToken(fileId, null, Arrays.asList()); -// SecurityContextHolder.getContext().setAuthentication(authentication); -// } - } - - filterChain.doFilter(request, response); - } catch (JwtAuthException jae) { - logger.debug(jae.getMessage()); - String errorJson = MessageUtil.makeJsonErrorMessage(MessageUtil.ERROR_CODE_AUTH_FAIL, jae.getMessage()); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json;charset=UTF-8"); - response.getWriter().write(errorJson); - } catch (Exception e) { - logger.error(e.getMessage()); - String errorJson = MessageUtil.makeJsonErrorMessage(MessageUtil.ERROR_CODE_AP_ERROR, e.getMessage()); - response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - response.setContentType("application/json;charset=UTF-8"); - response.getWriter().write(errorJson); - } - } -} - diff --git a/src/main/java/com/eactive/eai/authserver/custom/BearerTokenInfo.java b/src/main/java/com/eactive/eai/authserver/custom/BearerTokenInfo.java deleted file mode 100644 index e5a37fb..0000000 --- a/src/main/java/com/eactive/eai/authserver/custom/BearerTokenInfo.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.eactive.eai.authserver.custom; - -import java.util.Set; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class BearerTokenInfo { - @JsonProperty("client_id") - private String clientId; - private Long expiresIn; - private Long expiresAt; - private Set scopeSet; - - public String getClientId() { - return clientId; - } - - public void setClientId(String clientId) { - this.clientId = clientId; - } - - public Long getExpiresIn() { - return expiresIn; - } - - public void setExpiresIn(Long expiresIn) { - this.expiresIn = expiresIn; - } - - public Long getExpiresAt() { - return expiresAt; - } - - public void setExpiresAt(Long expiresIn) { - this.expiresAt = System.currentTimeMillis() + (expiresIn * 1000); - } - - public Set getScopeSet() { - return scopeSet; - } - - public void setScopeSet(Set scopeSet) { - this.scopeSet = scopeSet; - } - - public boolean isExpired() { - return System.currentTimeMillis() > expiresAt; - } -} diff --git a/src/main/java/com/eactive/eai/authserver/custom/BearerTokenService.java b/src/main/java/com/eactive/eai/authserver/custom/BearerTokenService.java deleted file mode 100644 index 3b01368..0000000 --- a/src/main/java/com/eactive/eai/authserver/custom/BearerTokenService.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.eactive.eai.authserver.custom; - -import java.security.SecureRandom; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import org.springframework.stereotype.Service; - -@Service -public class BearerTokenService { - - // CA 토큰 저장소 - private final Map CATokenStore = new ConcurrentHashMap<>(); - - // File 토큰 저장소 - private final Map fileTokenStore = new ConcurrentHashMap<>(); - -// // FileTransfer 토큰 저장소 -// private final Map fileTransferTokenStore = new ConcurrentHashMap<>(); - - public String generateCAToken(String clientId, Long expiresIn, Set scopeSet) { - String token = UUID.randomUUID().toString(); - - BearerTokenInfo CATokenInfo = new BearerTokenInfo(); - CATokenInfo.setClientId(clientId); - CATokenInfo.setExpiresIn(expiresIn); - CATokenInfo.setExpiresAt(expiresIn); - CATokenInfo.setScopeSet(scopeSet); - - CATokenStore.put(token, CATokenInfo); - - return token; - } - - public String generateFileToken(String fileId) { - //String token = UUID.randomUUID().toString().replace("-", ""); - String token = generateTokenString(987); - fileTokenStore.put(token, fileId); - return token; - } - -// public String generateFileTransferToken(String fileId) { -// //String token = UUID.randomUUID().toString().replace("-", ""); -// String token = generateTokenString(654); -// fileTransferTokenStore.put(token, fileId); -// return token; -// } - - public boolean validateCA(String token) { - BearerTokenInfo CATokenInfo = CATokenStore.get(token); - if (CATokenInfo == null) return false; - - if (CATokenInfo.isExpired()) { - CATokenStore.remove(token); - return false; - } - return CATokenStore.containsKey(token); - } - - public boolean validateFile(String token) { - return fileTokenStore.containsKey(token); - } - -// public boolean validateFileTransfer(String token) { -// return fileTransferTokenStore.containsKey(token); -// } - - public String getClientIdFromCA(String token) { - BearerTokenInfo CATokenInfo = CATokenStore.get(token); - return CATokenInfo.getClientId(); - } - - public String getFileIdFromFile(String token) { - return fileTokenStore.get(token); - } - -// public String getFileIdFromFileTransfer(String token) { -// return fileTransferTokenStore.get(token); -// } - - private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - private static final SecureRandom secureRandom = new SecureRandom(); - - public static String generateTokenString(int length) { - // Java 8의 IntStream과 Collectors.joining을 사용한 효율적인 방법 - return IntStream.range(0, length) - .map(i -> secureRandom.nextInt(CHARACTERS.length())) - .mapToObj(CHARACTERS::charAt) - .map(String::valueOf) - .collect(Collectors.joining()); - } -} - diff --git a/src/main/java/com/eactive/eai/authserver/custom/SecurityConfig.java b/src/main/java/com/eactive/eai/authserver/custom/SecurityConfig.java deleted file mode 100644 index f2a6183..0000000 --- a/src/main/java/com/eactive/eai/authserver/custom/SecurityConfig.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.eactive.eai.authserver.custom; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -// -//@Configuration -//@EnableWebSecurity -//public class SecurityConfig { -// -// private final BearerTokenFilter tokenFilter; -// -// public SecurityConfig(BearerTokenFilter tokenFilter) { -// this.tokenFilter = tokenFilter; -// } -// -// @Bean -// public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { -// -// http -// .csrf().disable() -// .authorizeRequests() -// .antMatchers("/auth/token/**").permitAll() -// .anyRequest().authenticated() -// .and() -// .addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class); -// -// return http.build(); -// } -//} -@Configuration -@EnableWebSecurity -public class SecurityConfig extends WebSecurityConfigurerAdapter { - - private final BearerTokenFilter tokenFilter; - - @Autowired - public SecurityConfig(BearerTokenFilter tokenFilter) { - this.tokenFilter = tokenFilter; - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - - http - .csrf().disable() - .authorizeRequests() - .antMatchers("/auth/token/**").permitAll() - .anyRequest().authenticated() - .and() - .addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class); - } -} \ No newline at end of file diff --git a/src/main/java/com/eactive/eai/custom/kjb/adapter/http/client/filter/KFTCFaceFilter.java b/src/main/java/com/eactive/eai/custom/kjb/adapter/http/client/filter/KFTCFaceFilter.java index d2b7d4f..78e3c70 100644 --- a/src/main/java/com/eactive/eai/custom/kjb/adapter/http/client/filter/KFTCFaceFilter.java +++ b/src/main/java/com/eactive/eai/custom/kjb/adapter/http/client/filter/KFTCFaceFilter.java @@ -16,6 +16,7 @@ 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; @@ -28,6 +29,12 @@ public class KFTCFaceFilter implements HttpClientAdapterFilter, HttpAdapterServi 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 @@ -47,7 +54,12 @@ public class KFTCFaceFilter implements HttpClientAdapterFilter, HttpAdapterServi } String request_datetime = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()); - String transaction_id = orgCode + request_datetime + "1" + RandomStringUtils.random(4, true, true).toUpperCase(); + 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) { diff --git a/src/main/java/com/eactive/eai/custom/kjb/adapter/http/client/filter/KFTCP2PFilter.java b/src/main/java/com/eactive/eai/custom/kjb/adapter/http/client/filter/KFTCP2PFilter.java index 92c0f36..270b2e6 100644 --- a/src/main/java/com/eactive/eai/custom/kjb/adapter/http/client/filter/KFTCP2PFilter.java +++ b/src/main/java/com/eactive/eai/custom/kjb/adapter/http/client/filter/KFTCP2PFilter.java @@ -11,6 +11,7 @@ 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; @@ -22,21 +23,29 @@ public class KFTCP2PFilter implements HttpClientAdapterFilter, HttpAdapterServic 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 = "034"; // 광주은행 행코드 + 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("yyyyMMddHHmmss").format(new Date()); - String apiTrxNo = orgCode + apiTrxDtm + "1" + RandomStringUtils.random(5, true, true).toUpperCase(); + + 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) { diff --git a/src/main/java/com/eactive/eai/custom/message/StandardMessageCoordinatorKJB.java b/src/main/java/com/eactive/eai/custom/message/StandardMessageCoordinatorKJB.java index 437fa14..f0bb6fc 100644 --- a/src/main/java/com/eactive/eai/custom/message/StandardMessageCoordinatorKJB.java +++ b/src/main/java/com/eactive/eai/custom/message/StandardMessageCoordinatorKJB.java @@ -87,7 +87,7 @@ public class StandardMessageCoordinatorKJB extends DefaultStandardMessageCoordin //전문요청일자 standardMessage.setData("Common.BizProc_Info.MESG_DMAN_DT", DatetimeUtil.getCurrentDate()); //전문요청시간 - standardMessage.setData("Common.BizProc_Info.MESG_DMAN_TKTM", DatetimeUtil.getTime8()); + standardMessage.setData("Common.BizProc_Info.MESG_DMAN_TKTM", DatetimeUtil.getFormattedDate("HHmmssSSS")); //2012-07-24 임의 로 영업일자에 system시간 설정 diff --git a/src/main/java/com/eactive/eai/custom/transformer/function/userdefined/JsonPathExtractRequestMessage.java b/src/main/java/com/eactive/eai/custom/transformer/function/userdefined/JsonPathExtractRequestMessage.java new file mode 100644 index 0000000..4447414 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/transformer/function/userdefined/JsonPathExtractRequestMessage.java @@ -0,0 +1,74 @@ +package com.eactive.eai.custom.transformer.function.userdefined; + +import com.eactive.eai.common.context.ElinkTransactionContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider; +import com.jayway.jsonpath.spi.json.JacksonJsonProvider; +import org.nfunk.jep.ParseException; +import org.nfunk.jep.function.PostfixMathCommand; + +import java.util.Stack; + +public class JsonPathExtractRequestMessage extends PostfixMathCommand { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public JsonPathExtractRequestMessage() { + numberOfParameters = -1; + } + + public void run(Stack inStack) throws ParseException { + checkStack(inStack); + + String bizData = ElinkTransactionContext.getRequestBizData(); + + Object p1 = inStack.pop(); + String path = String.valueOf(p1); + + inStack.clear(); + + try { + // Jackson을 JsonPath의 JSON 프로바이더로 설정 + Configuration config = Configuration.builder() + .jsonProvider(new JacksonJsonNodeJsonProvider()) + .build(); + + JsonNode resultNode = JsonPath.using(config).parse(bizData).read(path); + + if (resultNode == null) { + inStack.push(null); + } else if (resultNode.isValueNode()) { + // 단순 값(문자열, 숫자, boolean 등)은 텍스트로 반환 + inStack.push(resultNode.asText()); + } else { + // 객체/배열은 JsonNode 그대로 반환 + inStack.push(resultNode); + } + } catch (Exception e) { + throw new ParseException("JsonPath extract error: " + e.getMessage()); + } + } + + private JsonNode getNodeAtPath(JsonNode rootNode, String path) { + String[] tokens = path.split("/"); + JsonNode currentNode = rootNode; + + for (String token : tokens) { + if (token.isEmpty()) continue; + + if (token.matches("\\d+")) { + currentNode = currentNode.get(Integer.parseInt(token)); + } else { + currentNode = currentNode.get(token); + } + + if (currentNode == null) { + return null; + } + } + return currentNode; + } +} diff --git a/src/main/java/com/eactive/eai/custom/transformer/function/userdefined/SubStringRequestMessage.java b/src/main/java/com/eactive/eai/custom/transformer/function/userdefined/SubStringRequestMessage.java new file mode 100644 index 0000000..add30f3 --- /dev/null +++ b/src/main/java/com/eactive/eai/custom/transformer/function/userdefined/SubStringRequestMessage.java @@ -0,0 +1,41 @@ +package com.eactive.eai.custom.transformer.function.userdefined; + +import com.eactive.eai.common.context.ElinkTransactionContext; +import org.apache.commons.lang.StringUtils; +import org.nfunk.jep.ParseException; +import org.nfunk.jep.function.PostfixMathCommand; + +import java.util.Stack; + +public class SubStringRequestMessage extends PostfixMathCommand { + public SubStringRequestMessage() { + numberOfParameters = -1; + } + + public void run(Stack inStack) throws ParseException { + checkStack(inStack); + + String bizData = ElinkTransactionContext.getRequestBizData(); + + Object p1 = inStack.pop(); + Object p2 = inStack.pop(); + + int start = toInt(p2); + int end = toInt(p1); + + String resultStr = bizData.substring(start, end); + inStack.clear(); + + inStack.push(resultStr); + } + + public static int toInt(Object obj) { + if (obj instanceof Number) { + return ((Number) obj).intValue(); + } + if (obj instanceof String) { + return Integer.parseInt(((String) obj).trim()); + } + throw new IllegalArgumentException("Cannot convert to int: " + obj); + } +}