commit f9d430ade6c66b17d916db8ab249ef3acb77dd57 Author: 박성영 Date: Tue Nov 4 18:23:11 2025 +0900 깃 이그노어 추가 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1de4b95 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +/gradle/wrapper/gradle-wrapper.jar +/gradle/wrapper/gradle-wrapper.properties +/.gradle/8.14/checksums/checksums.lock +/.gradle/8.14/checksums/md5-checksums.bin +/.gradle/8.14/checksums/sha1-checksums.bin +/.gradle/8.14/executionHistory/executionHistory.bin +/.gradle/8.14/executionHistory/executionHistory.lock +/.gradle/8.14/fileChanges/last-build.bin +/.gradle/8.14/fileHashes/fileHashes.bin +/.gradle/8.14/fileHashes/fileHashes.lock +/.gradle/8.14/fileHashes/resourceHashesCache.bin +/.gradle/8.14/gc.properties +/.gradle/buildOutputCleanup/buildOutputCleanup.lock +/.gradle/buildOutputCleanup/cache.properties +/.gradle/buildOutputCleanup/outputFiles.bin +/.gradle/vcs-1/gc.properties +/.gradle/file-system.probe +/gradlew.bat +/gradlew +/.idea/ +/.gradle/ +/build/ +/gradle/ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..b048499 --- /dev/null +++ b/build.gradle @@ -0,0 +1,49 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.3.4' + id 'io.spring.dependency-management' version '1.1.6' +} + +group = 'com.vmis' +version = '0.0.1-SNAPSHOT' +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + + // OpenAPI/Swagger UI + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' + + // Apache HttpClient5 for RestTemplate request factory + implementation 'org.apache.httpcomponents.client5:httpclient5:5.2.3' + + // GPKI JNI local library + implementation files('lib/libgpkiapi_jni_1.5.jar') + + // Log4j 1.x for legacy util (NewGpkiUtil). Consider replacing with SLF4J in future. + implementation 'log4j:log4j:1.2.17' + + // Configuration metadata + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' + + // Lombok + compileOnly 'org.projectlombok:lombok:1.18.34' + annotationProcessor 'org.projectlombok:lombok:1.18.34' + testCompileOnly 'org.projectlombok:lombok:1.18.34' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.34' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +tasks.withType(Test).configureEach { + useJUnitPlatform() +} diff --git a/lib/libgpkiapi_jni_1.5.jar b/lib/libgpkiapi_jni_1.5.jar new file mode 100644 index 0000000..5e3bdea Binary files /dev/null and b/lib/libgpkiapi_jni_1.5.jar differ diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..36c9855 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'VMIS-interface' diff --git a/src/main/java/com/vmis/interfaceapp/VmisInterfaceApplication.java b/src/main/java/com/vmis/interfaceapp/VmisInterfaceApplication.java new file mode 100644 index 0000000..a204e6b --- /dev/null +++ b/src/main/java/com/vmis/interfaceapp/VmisInterfaceApplication.java @@ -0,0 +1,11 @@ +package com.vmis.interfaceapp; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class VmisInterfaceApplication { + public static void main(String[] args) { + SpringApplication.run(VmisInterfaceApplication.class, args); + } +} diff --git a/src/main/java/com/vmis/interfaceapp/client/GovernmentApiClient.java b/src/main/java/com/vmis/interfaceapp/client/GovernmentApiClient.java new file mode 100644 index 0000000..97da794 --- /dev/null +++ b/src/main/java/com/vmis/interfaceapp/client/GovernmentApiClient.java @@ -0,0 +1,781 @@ +package com.vmis.interfaceapp.client; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.vmis.interfaceapp.config.properties.VmisProperties; +import com.vmis.interfaceapp.gpki.GpkiService; +import com.vmis.interfaceapp.model.basic.BasicRequest; +import com.vmis.interfaceapp.model.basic.BasicResponse; +import com.vmis.interfaceapp.model.common.Envelope; +import com.vmis.interfaceapp.model.ledger.LedgerRequest; +import com.vmis.interfaceapp.model.ledger.LedgerResponse; +import com.vmis.interfaceapp.util.TxIdUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestTemplate; + +import java.nio.charset.StandardCharsets; + +/** + * 정부 시스템 API 클라이언트 + * + *

이 클래스는 시군구연계 자동차 정보 조회를 위해 정부 시스템의 API를 호출하는 + * 클라이언트 역할을 수행합니다. HTTP 통신, 암호화, 에러 처리 등 정부 API와의 + * 모든 상호작용을 캡슐화합니다.

+ * + *

주요 책임:

+ * + * + *

아키텍처 패턴:

+ * + * + *

보안 특성:

+ * + * + * @see RestTemplate + * @see GpkiService + * @see VmisProperties + */ +@Component +public class GovernmentApiClient { + + /** + * SLF4J 로거 인스턴스 + * + *

모든 API 호출, 에러, 응답에 대한 로그를 기록합니다. + * 로그 레벨별 용도:

+ * + */ + private static final Logger log = LoggerFactory.getLogger(GovernmentApiClient.class); + + /** + * Spring RestTemplate + * + *

HTTP 클라이언트로서 실제 네트워크 통신을 수행합니다. + * 이 객체는 Spring Bean으로 주입되며, 설정에 따라 다음을 포함할 수 있습니다:

+ * + */ + private final RestTemplate restTemplate; + + /** + * VMIS 설정 속성 + * + *

application.yml 또는 application.properties에서 로드된 설정값들입니다. + * 포함되는 주요 설정:

+ * + */ + private final VmisProperties props; + + /** + * GPKI(행정전자서명) 서비스 + * + *

정부24 등 공공기관 간 통신에 사용되는 암호화 서비스입니다. + * 주요 기능:

+ * + * + *

암호화가 비활성화된 경우 평문(Plain Text)으로 통신합니다.

+ */ + private final GpkiService gpkiService; + + /** + * Jackson ObjectMapper + * + *

Java 객체와 JSON 문자열 간의 변환을 담당합니다. + * 주요 역할:

+ * + * + *

Spring Boot가 자동 구성한 ObjectMapper를 주입받아 사용하므로 + * 전역 설정(날짜 포맷, 네이밍 전략 등)이 일관되게 적용됩니다.

+ */ + private final ObjectMapper objectMapper; + + /** + * 서비스 타입 열거형 + * + *

정부 API 서비스의 종류를 구분하는 열거형입니다. + * 각 서비스 타입은 서로 다른 엔드포인트와 API 키를 가집니다.

+ * + *

서비스 타입:

+ * + */ + public enum ServiceType { + /** + * Basic service type. + */ + BASIC, + /** + * Ledger service type. + */ + LEDGER } + + /** + * 생성자를 통한 의존성 주입 + * + *

Spring의 생성자 주입 방식을 사용하여 필요한 모든 의존성을 주입받습니다. + * + * @param restTemplate HTTP 통신을 위한 RestTemplate 객체 + * @param props VMIS 애플리케이션 설정 속성 + * @param gpkiService GPKI 암호화/복호화 서비스 + * @param objectMapper JSON 직렬화/역직렬화를 위한 ObjectMapper + * @Component 어노테이션과 함께 사용되어 자동으로 Spring Bean으로 등록됩니다.

생성자 주입의 장점:

+ */ + public GovernmentApiClient(RestTemplate restTemplate, VmisProperties props, GpkiService gpkiService, ObjectMapper objectMapper) { + this.restTemplate = restTemplate; + this.props = props; + this.gpkiService = gpkiService; + this.objectMapper = objectMapper; + } + + /** + * 정부 API 호출 (문자열 기반) + * + *

이 메서드는 JSON 문자열을 직접 받아 정부 API를 호출하는 저수준(Low-level) 메서드입니다. + * 타입 안전성이 필요한 경우 {@link #callModel} 메서드를 사용하는 것이 권장됩니다.

+ * + *

처리 흐름:

+ *
    + *
  1. 설정 로드: 서비스 타입에 따라 적절한 정부 API 설정을 가져옴
  2. + *
  3. URL 구성: 호스트, 포트, 경로를 결합하여 완전한 URL 생성
  4. + *
  5. 트랜잭션 ID 생성: 요청 추적을 위한 고유 ID 생성 (TxIdUtil 사용)
  6. + *
  7. 헤더 구성: 필수 헤더(API 키, 시스템 정보, GPKI 설정 등) 추가
  8. + *
  9. 암호화 처리: GPKI가 활성화된 경우 요청 바디를 암호화
  10. + *
  11. HTTP 요청: RestTemplate을 사용하여 POST 요청 전송
  12. + *
  13. 복호화 처리: 성공 응답이고 GPKI가 활성화된 경우 응답 바디를 복호화
  14. + *
  15. 응답 반환: 최종 응답을 ResponseEntity로 반환
  16. + *
+ * + *

에러 처리 전략:

+ * + * + *

로깅:

+ * + * + * @param type 서비스 타입 (BASIC 또는 LEDGER) + * @param jsonBody 전송할 JSON 문자열 (암호화 전 평문) + * @return ResponseEntity<String> 정부 API 응답 (복호화 완료된 JSON 문자열) + * @throws RuntimeException GPKI 암호화/복호화 실패 시 + */ + public ResponseEntity call(ServiceType type, String jsonBody) { + // 1. 서비스 타입에 따른 설정 로드 + // props.getGov()는 정부 API 관련 모든 설정을 포함하는 객체를 반환 + VmisProperties.GovProps gov = props.getGov(); + + // 삼항 연산자를 사용하여 BASIC 또는 LEDGER 서비스 설정 선택 + VmisProperties.GovProps.Service svc = (type == ServiceType.BASIC) + ? gov.getServices().getBasic() + : gov.getServices().getLedger(); + + // 2. 완전한 URL 구성 (예: https://api.gov.kr:8080/vmis/basic) + String url = gov.buildServiceUrl(svc.getPath()); + + // 3. 트랜잭션 ID 생성 (요청 추적 및 디버깅 용도) + // 일반적으로 타임스탬프 + UUID 조합으로 생성됨 + String txId = TxIdUtil.generate(); + + // 4. HTTP 헤더 구성 + // API 키, 시스템 정보, GPKI 설정 등 필수 헤더 포함 + HttpHeaders headers = buildHeaders(svc, txId); + + // 5. GPKI 암호화 처리 + String bodyToSend = jsonBody; + if (gpkiService.isEnabled()) { + try { + // 평문 JSON을 암호화된 문자열로 변환 + // 암호화 결과는 Base64 인코딩된 문자열 + bodyToSend = gpkiService.encrypt(jsonBody); + } catch (Exception e) { + // 암호화 실패는 치명적 오류이므로 즉시 예외 발생 + throw new RuntimeException("GPKI 암호화 실패", e); + } + } + + // 6. HTTP 엔티티 생성 (헤더 + 바디) + HttpEntity request = new HttpEntity<>(bodyToSend, headers); + + // 7. 요청 로그 기록 + // 디버깅을 위해 URL, 트랜잭션 ID, GPKI 활성화 여부, 바디 길이 로깅 + log.info("[GOV-REQ] url={}, tx_id={}, gpki={}, length={}", url, txId, gpkiService.isEnabled(), bodyToSend != null ? bodyToSend.length() : 0); + + try { + // 8. 실제 HTTP POST 요청 전송 + // RestTemplate.exchange()는 HTTP 메서드, 헤더, 바디를 모두 지정 가능 + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, request, String.class); + String respBody = response.getBody(); + + // 9. 성공 응답인 경우 GPKI 복호화 처리 + if (gpkiService.isEnabled() && response.getStatusCode().is2xxSuccessful()) { + try { + // 암호화된 응답을 평문 JSON으로 복호화 + String decrypted = gpkiService.decrypt(respBody); + // 복호화된 바디로 새로운 ResponseEntity 생성 + // 원본 응답의 상태 코드와 헤더는 그대로 유지 + return ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()).body(decrypted); + } catch (Exception e) { + // 복호화 실패는 치명적 오류이므로 즉시 예외 발생 + throw new RuntimeException("GPKI 복호화 실패", e); + } + } + // GPKI가 비활성화되어 있거나 에러 응답인 경우 원본 그대로 반환 + return response; + } catch (HttpStatusCodeException ex) { + // 10. HTTP 에러 처리 (4xx, 5xx 상태 코드) + // 경고 로그 기록 (에러는 정상적인 비즈니스 흐름일 수 있으므로 WARN 레벨) + log.warn("[GOV-ERR] status={}, body={}", ex.getStatusCode(), ex.getResponseBodyAsString()); + + // 에러 응답을 ResponseEntity로 변환하여 반환 + // 호출자가 상태 코드와 에러 메시지를 확인할 수 있도록 함 + // 헤더가 null인 경우를 대비하여 빈 HttpHeaders 사용 + return ResponseEntity.status(ex.getStatusCode()).headers(ex.getResponseHeaders() != null ? ex.getResponseHeaders() : new HttpHeaders()).body(ex.getResponseBodyAsString()); + } + } + + /** + * HTTP 헤더 구성 + * + *

정부 API 호출에 필요한 모든 HTTP 헤더를 구성하는 private 메서드입니다. + * 정부 시스템은 엄격한 헤더 검증을 수행하므로 모든 필수 헤더가 정확히 포함되어야 합니다.

+ * + *

헤더 구성 항목:

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
헤더명설명예시값필수여부
Content-Type요청 바디의 미디어 타입 및 문자 인코딩application/json; charset=UTF-8필수
Accept클라이언트가 수용 가능한 응답 형식application/json필수
gpki_ynGPKI 암호화 사용 여부 (Y/N)Y필수
tx_id트랜잭션 고유 ID (요청 추적용)20250104123045_abc123필수
cert_server_id인증서 서버 식별자VMIS_SERVER_01필수
api_key서비스별 API 인증 키abc123def456...필수
cvmis_apikeyCVMIS 시스템 API 키xyz789uvw012...필수
INFO_SYS_ID정보시스템 식별자VMIS_SEOUL필수
REGION_CODE지역 코드 (시군구 코드)11010 (서울 종로구)선택
DEPT_CODE부서 코드TRANS_001선택
+ * + *

문자 인코딩:

+ *
    + *
  • Content-Type에 UTF-8 인코딩을 명시적으로 지정
  • + *
  • 한글 데이터 처리를 위해 필수
  • + *
  • 정부 시스템이 다양한 클라이언트와 호환되도록 표준 인코딩 사용
  • + *
+ * + *

보안 고려사항:

+ *
    + *
  • API 키는 설정 파일에서 안전하게 관리
  • + *
  • 로그에 API 키가 노출되지 않도록 주의
  • + *
  • 각 서비스(BASIC, LEDGER)마다 별도의 API 키 사용 가능
  • + *
+ * + * @param svc 서비스 설정 객체 (API 키, 경로 등 포함) + * @param txId 이번 요청의 트랜잭션 ID + * @return HttpHeaders 구성된 HTTP 헤더 객체 + */ + private HttpHeaders buildHeaders(VmisProperties.GovProps.Service svc, String txId) { + // 1. 빈 HttpHeaders 객체 생성 + HttpHeaders headers = new HttpHeaders(); + + // 2. Content-Type 설정 + // UTF-8 인코딩을 명시하여 한글 데이터가 올바르게 전송되도록 함 + headers.setContentType(new MediaType("application", "json", StandardCharsets.UTF_8)); + + // 3. Accept 헤더 설정 + // 서버에게 JSON 형식의 응답을 요청함 + headers.setAccept(java.util.List.of(MediaType.APPLICATION_JSON)); + + // 4. GPKI 암호화 사용 여부 + // 정부 서버가 요청 바디 복호화 여부를 결정하는 데 사용 + headers.add("gpki_yn", gpkiService.isEnabled() ? "Y" : "N"); + + // 5. 트랜잭션 ID + // 요청 추적, 로그 연관, 문제 해결 시 사용 + headers.add("tx_id", txId); + + // 6. 인증서 서버 ID + // GPKI 인증서를 발급받은 서버의 식별자 + headers.add("cert_server_id", props.getGpki().getCertServerId()); + + // 7. API 인증 키 + // 서비스별로 다른 API 키 사용 가능 (BASIC과 LEDGER 각각) + headers.add("api_key", svc.getApiKey()); + + // 8. CVMIS API 키 + // CVMIS(Car Vehicle Management Information System) 전용 API 키 + headers.add("cvmis_apikey", svc.getCvmisApikey()); + + // 9. 정보시스템 ID + // 호출하는 시스템을 식별하는 필수 값 + headers.add("INFO_SYS_ID", props.getSystem().getInfoSysId()); + + // 10. 지역 코드 (선택 사항) + // 시군구 코드 (예: 11010 = 서울특별시 종로구) + // null이 아닌 경우에만 헤더에 추가 + if (props.getSystem().getRegionCode() != null) headers.add("REGION_CODE", props.getSystem().getRegionCode()); + + // 11. 부서 코드 (선택 사항) + // 호출하는 부서의 코드 + // null이 아닌 경우에만 헤더에 추가 + if (props.getSystem().getDepartmentCode() != null) headers.add("DEPT_CODE", props.getSystem().getDepartmentCode()); + + // 구성 완료된 헤더 반환 + return headers; + } + + /** + * 자동차 기본사항 조회 API 호출 + * + *

타입 안전성이 보장되는 자동차 기본사항 조회 메서드입니다. + * 내부적으로 {@link #callModel}을 호출하여 실제 통신을 수행합니다.

+ * + *

특징:

+ *
    + *
  • 제네릭 타입으로 컴파일 타임 타입 체크
  • + *
  • 요청/응답 객체가 Envelope로 감싸져 있음
  • + *
  • Jackson TypeReference를 사용한 제네릭 역직렬화
  • + *
+ * + *

사용 예시:

+ *
+     * BasicRequest request = new BasicRequest();
+     * request.setVehicleNo("12가3456");
+     *
+     * Envelope<BasicRequest> envelope = new Envelope<>();
+     * envelope.setData(request);
+     *
+     * ResponseEntity<Envelope<BasicResponse>> response = govClient.callBasic(envelope);
+     * BasicResponse data = response.getBody().getData();
+     * 
+ * + * @param envelope 자동차 기본사항 조회 요청을 담은 Envelope + * @return ResponseEntity<Envelope<BasicResponse>> 조회 결과를 담은 응답 + */ + public ResponseEntity> callBasic(Envelope envelope) { + // TypeReference를 사용하여 제네릭 타입 정보 전달 + // 익명 클래스를 생성하여 타입 소거(Type Erasure) 문제 해결 + return callModel(ServiceType.BASIC, envelope, new TypeReference>(){}); + } + + /** + * 자동차 등록원부(갑) 조회 API 호출 + * + *

타입 안전성이 보장되는 자동차 등록원부 조회 메서드입니다. + * 내부적으로 {@link #callModel}을 호출하여 실제 통신을 수행합니다.

+ * + *

특징:

+ *
    + *
  • 제네릭 타입으로 컴파일 타임 타입 체크
  • + *
  • 요청/응답 객체가 Envelope로 감싸져 있음
  • + *
  • Jackson TypeReference를 사용한 제네릭 역직렬화
  • + *
+ * + *

사용 예시:

+ *
+     * LedgerRequest request = new LedgerRequest();
+     * request.setVehicleNo("12가3456");
+     * request.setOwnerName("홍길동");
+     *
+     * Envelope<LedgerRequest> envelope = new Envelope<>();
+     * envelope.setData(request);
+     *
+     * ResponseEntity<Envelope<LedgerResponse>> response = govClient.callLedger(envelope);
+     * LedgerResponse data = response.getBody().getData();
+     * 
+ * + * @param envelope 자동차 등록원부 조회 요청을 담은 Envelope + * @return ResponseEntity<Envelope<LedgerResponse>> 조회 결과를 담은 응답 + */ + public ResponseEntity> callLedger(Envelope envelope) { + // TypeReference를 사용하여 제네릭 타입 정보 전달 + // 익명 클래스를 생성하여 타입 소거(Type Erasure) 문제 해결 + return callModel(ServiceType.LEDGER, envelope, new TypeReference>(){}); + } + + /** + * 정부 API 호출 (타입 안전 모델 기반) + * + *

이 메서드는 정부 API 호출의 핵심 로직을 담고 있는 제네릭 메서드입니다. + * Java 객체를 받아 JSON으로 변환하고, 암호화하여 전송한 후, 응답을 복호화하여 + * 다시 Java 객체로 변환하는 전체 파이프라인을 처리합니다.

+ * + *

Template Method 패턴:

+ *
    + *
  • 이 메서드는 Template Method 패턴의 템플릿 역할
  • + *
  • 공통 처리 흐름을 정의하고 서비스별 차이는 파라미터로 처리
  • + *
  • 코드 중복을 제거하고 일관성을 보장
  • + *
+ * + *

제네릭 타입 파라미터:

+ *
    + *
  • <TReq>: 요청 데이터 타입 (BasicRequest 또는 LedgerRequest)
  • + *
  • <TResp>: 응답 데이터 타입 (BasicResponse 또는 LedgerResponse)
  • + *
  • 타입 안전성을 보장하여 런타임 에러 방지
  • + *
+ * + *

처리 흐름 (상세):

+ *
    + *
  1. 설정 로드: + *
    • 서비스 타입에 따라 BASIC 또는 LEDGER 설정 선택
    + *
  2. + *
  3. URL 및 트랜잭션 ID 구성: + *
    • 완전한 API URL 생성
    • + *
    • 고유 트랜잭션 ID 생성
    + *
  4. + *
  5. 직렬화 (Serialization): + *
    • Java 객체(Envelope<TReq>)를 JSON 문자열로 변환
    • + *
    • ObjectMapper.writeValueAsString() 사용
    + *
  6. + *
  7. 헤더 구성: + *
    • buildHeaders() 메서드 호출
    • + *
    • 모든 필수 헤더 추가
    + *
  8. + *
  9. GPKI 암호화 (선택적): + *
    • GPKI가 활성화된 경우 JSON을 암호화
    • + *
    • gpkiEncrypt() 메서드 호출
    + *
  10. + *
  11. HTTP 요청 전송: + *
    • RestTemplate.exchange()로 POST 요청
    • + *
    • 요청 로그 기록
    + *
  12. + *
  13. GPKI 복호화 (선택적): + *
    • 성공 응답(2xx)이고 GPKI가 활성화된 경우
    • + *
    • gpkiDecrypt() 메서드 호출
    + *
  14. + *
  15. 역직렬화 (Deserialization): + *
    • JSON 문자열을 Java 객체(Envelope<TResp>)로 변환
    • + *
    • TypeReference를 사용하여 제네릭 타입 정보 보존
    + *
  16. + *
  17. 응답 반환: + *
    • ResponseEntity로 감싸서 HTTP 정보 포함
    + *
  18. + *
+ * + *

에러 처리 전략 (3단계):

+ *
    + *
  1. HttpStatusCodeException (HTTP 에러): + *
      + *
    • 정부 API가 4xx 또는 5xx 상태 코드를 반환한 경우
    • + *
    • 에러 응답 바디를 파싱하여 Envelope 객체로 변환 시도
    • + *
    • 파싱 실패 시 빈 Envelope 객체 반환
    • + *
    • 에러 로그 기록 (WARN 레벨)
    • + *
    + *
  2. + *
  3. JSON 파싱 에러: + *
      + *
    • 응답 JSON이 예상한 형식과 다른 경우
    • + *
    • RuntimeException으로 래핑하여 상위로 전파
    • + *
    + *
  4. + *
  5. 기타 예외: + *
      + *
    • 네트워크 타임아웃, 연결 실패 등
    • + *
    • RuntimeException으로 래핑하여 상위로 전파
    • + *
    + *
  6. + *
+ * + *

TypeReference의 필요성:

+ *

Java의 제네릭은 런타임에 타입 소거(Type Erasure)가 발생하여 + * {@code objectMapper.readValue(json, Envelope.class)}와 같은 코드는 + * 컴파일되지 않습니다. TypeReference는 익명 클래스를 사용하여 컴파일 타임의 + * 제네릭 타입 정보를 런타임에 전달하는 Jackson의 메커니즘입니다.

+ * + *

로깅 정보:

+ *
    + *
  • 요청 로그: [GOV-REQ] url, tx_id, gpki, length
  • + *
  • 에러 로그: [GOV-ERR] status, body
  • + *
+ * + * @param 요청 데이터의 제네릭 타입 + * @param 응답 데이터의 제네릭 타입 + * @param type 서비스 타입 (BASIC 또는 LEDGER) + * @param envelope 요청 데이터를 담은 Envelope 객체 + * @param respType 응답 타입에 대한 TypeReference (제네릭 타입 정보 보존용) + * @return ResponseEntity<Envelope<TResp>> 응답 데이터를 담은 ResponseEntity + * @throws RuntimeException JSON 직렬화/역직렬화 실패, 네트워크 오류, GPKI 암복호화 실패 등 + */ + private ResponseEntity> callModel(ServiceType type, + Envelope envelope, + TypeReference> respType) { + // 1. 서비스 타입에 따른 설정 로드 + VmisProperties.GovProps gov = props.getGov(); + VmisProperties.GovProps.Service svc = (type == ServiceType.BASIC) + ? gov.getServices().getBasic() + : gov.getServices().getLedger(); + + // 2. URL 및 트랜잭션 ID 생성 + String url = gov.buildServiceUrl(svc.getPath()); + String txId = TxIdUtil.generate(); + + try { + // 3. 직렬화: Java 객체 → JSON 문자열 + // ObjectMapper가 Envelope 객체를 JSON으로 변환 + // 날짜, null 값 등의 처리는 ObjectMapper 설정에 따름 + String jsonBody = objectMapper.writeValueAsString(envelope); + + // 4. HTTP 헤더 구성 + HttpHeaders headers = buildHeaders(svc, txId); + + // 5. GPKI 암호화 처리 + String bodyToSend = jsonBody; + if (gpkiService.isEnabled()) { + // JSON 평문을 암호화된 문자열로 변환 + bodyToSend = gpkiEncrypt(jsonBody); + } + + // 6. HTTP 엔티티 생성 (헤더 + 바디) + HttpEntity request = new HttpEntity<>(bodyToSend, headers); + + // 7. 요청 로그 기록 + log.info("[GOV-REQ] url={}, tx_id={}, gpki={}, length={}", url, txId, gpkiService.isEnabled(), bodyToSend != null ? bodyToSend.length() : 0); + + // 8. 실제 HTTP POST 요청 전송 + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, request, String.class); + String respBody = response.getBody(); + + // 9. GPKI 복호화 처리 (성공 응답인 경우만) + if (gpkiService.isEnabled() && response.getStatusCode().is2xxSuccessful()) { + // 암호화된 응답을 평문 JSON으로 복호화 + respBody = gpkiDecrypt(respBody); + } + + // 10. 역직렬화: JSON 문자열 → Java 객체 + // TypeReference를 사용하여 제네릭 타입 정보 전달 + Envelope mapped = objectMapper.readValue(respBody, respType); + + // 11. 응답 반환 (상태 코드, 헤더, 바디 모두 포함) + return ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()).body(mapped); + + } catch (HttpStatusCodeException ex) { + // HTTP 에러 처리 (4xx, 5xx) + log.warn("[GOV-ERR] status={}, body={}", ex.getStatusCode(), ex.getResponseBodyAsString()); + + // 에러 응답 바디 파싱 시도 + // 정부 API는 에러 응답도 Envelope 형식으로 반환할 수 있음 + Envelope empty = new Envelope<>(); + try { + // 에러 응답을 Envelope 객체로 파싱 + Envelope parsed = objectMapper.readValue(ex.getResponseBodyAsString(), respType); + return ResponseEntity.status(ex.getStatusCode()).headers(ex.getResponseHeaders() != null ? ex.getResponseHeaders() : new HttpHeaders()).body(parsed); + } catch (Exception parseEx) { + // 파싱 실패 시 빈 Envelope 반환 + // 호출자는 HTTP 상태 코드로 에러 판단 가능 + return ResponseEntity.status(ex.getStatusCode()).headers(ex.getResponseHeaders() != null ? ex.getResponseHeaders() : new HttpHeaders()).body(empty); + } + } catch (Exception e) { + // 기타 모든 예외 (네트워크 오류, JSON 파싱 오류, GPKI 오류 등) + // RuntimeException으로 래핑하여 상위로 전파 + // Spring의 @ExceptionHandler에서 처리 가능 + throw new RuntimeException("정부 API 호출 중 오류", e); + } + } + + /** + * GPKI 암호화 처리 + * + *

평문 JSON 문자열을 GPKI(행정전자서명) 알고리즘으로 암호화하는 헬퍼 메서드입니다. + * 암호화 실패 시 명확한 에러 메시지와 함께 RuntimeException을 발생시킵니다.

+ * + *

암호화 과정:

+ *
    + *
  1. 평문 JSON 문자열을 바이트 배열로 변환
  2. + *
  3. 정부 시스템의 공개키를 사용하여 암호화
  4. + *
  5. 암호화된 바이트 배열을 Base64로 인코딩
  6. + *
  7. Base64 문자열 반환
  8. + *
+ * + *

에러 처리:

+ *
    + *
  • 인증서 오류: GPKI 인증서가 유효하지 않거나 만료된 경우
  • + *
  • 암호화 오류: 암호화 알고리즘 실행 중 오류 발생
  • + *
  • 인코딩 오류: Base64 인코딩 실패
  • + *
  • 모든 예외는 RuntimeException으로 래핑하여 즉시 중단
  • + *
+ * + *

보안 고려사항:

+ *
    + *
  • 공개키 암호화 방식 사용 (비대칭키)
  • + *
  • 정부 시스템만 개인키로 복호화 가능
  • + *
  • 민감한 개인정보 보호
  • + *
+ * + * @param jsonBody 암호화할 평문 JSON 문자열 + * @return String Base64로 인코딩된 암호화 문자열 + * @throws RuntimeException GPKI 암호화 실패 시 + */ + private String gpkiEncrypt(String jsonBody) { + try { + // GpkiService에 암호화 위임 + // 실제 암호화 로직은 GpkiService가 캡슐화 + return gpkiService.encrypt(jsonBody); + } catch (Exception e) { + // 암호화 실패는 치명적 오류 + // 평문 데이터를 전송할 수 없으므로 즉시 중단 + throw new RuntimeException("GPKI 암호화 실패", e); + } + } + + /** + * GPKI 복호화 처리 + * + *

암호화된 응답 문자열을 GPKI(행정전자서명) 알고리즘으로 복호화하는 헬퍼 메서드입니다. + * 복호화 실패 시 명확한 에러 메시지와 함께 RuntimeException을 발생시킵니다.

+ * + *

복호화 과정:

+ *
    + *
  1. Base64로 인코딩된 암호문을 바이트 배열로 디코딩
  2. + *
  3. 우리 시스템의 개인키를 사용하여 복호화
  4. + *
  5. 복호화된 바이트 배열을 UTF-8 문자열로 변환
  6. + *
  7. 평문 JSON 문자열 반환
  8. + *
+ * + *

에러 처리:

+ *
    + *
  • 인증서 오류: GPKI 인증서가 유효하지 않거나 만료된 경우
  • + *
  • 복호화 오류: 암호문이 손상되었거나 올바르지 않은 경우
  • + *
  • 디코딩 오류: Base64 디코딩 실패
  • + *
  • 문자 인코딩 오류: UTF-8 변환 실패
  • + *
  • 모든 예외는 RuntimeException으로 래핑하여 즉시 중단
  • + *
+ * + *

보안 고려사항:

+ *
    + *
  • 개인키 암호화 방식 사용 (비대칭키)
  • + *
  • 개인키는 안전하게 저장 및 관리 필요
  • + *
  • 복호화 실패 시 상세 에러 정보 로깅 주의 (정보 유출 방지)
  • + *
+ * + * @param cipher Base64로 인코딩된 암호화 문자열 + * @return String 복호화된 평문 JSON 문자열 + * @throws RuntimeException GPKI 복호화 실패 시 + */ + private String gpkiDecrypt(String cipher) { + try { + // GpkiService에 복호화 위임 + // 실제 복호화 로직은 GpkiService가 캡슐화 + return gpkiService.decrypt(cipher); + } catch (Exception e) { + // 복호화 실패는 치명적 오류 + // 암호문을 해석할 수 없으므로 즉시 중단 + throw new RuntimeException("GPKI 복호화 실패", e); + } + } +} diff --git a/src/main/java/com/vmis/interfaceapp/config/GpkiConfig.java b/src/main/java/com/vmis/interfaceapp/config/GpkiConfig.java new file mode 100644 index 0000000..97af523 --- /dev/null +++ b/src/main/java/com/vmis/interfaceapp/config/GpkiConfig.java @@ -0,0 +1,20 @@ +package com.vmis.interfaceapp.config; + +import com.vmis.interfaceapp.config.properties.VmisProperties; +import com.vmis.interfaceapp.gpki.GpkiService; +import com.vmis.interfaceapp.gpki.NoopGpkiService; +import com.vmis.interfaceapp.gpki.RealGpkiService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class GpkiConfig { + + @Bean + public GpkiService gpkiService(VmisProperties properties) { + if (properties.getGpki().isEnabledFlag()) { + return new RealGpkiService(properties); + } + return new NoopGpkiService(); + } +} diff --git a/src/main/java/com/vmis/interfaceapp/config/HttpClientConfig.java b/src/main/java/com/vmis/interfaceapp/config/HttpClientConfig.java new file mode 100644 index 0000000..5d24686 --- /dev/null +++ b/src/main/java/com/vmis/interfaceapp/config/HttpClientConfig.java @@ -0,0 +1,46 @@ +package com.vmis.interfaceapp.config; + +import com.vmis.interfaceapp.config.properties.VmisProperties; +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.HttpClientBuilder; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +import java.time.Duration; + +@Configuration +public class HttpClientConfig { + + @Bean + public RestTemplate restTemplate(VmisProperties props, RestTemplateBuilder builder) { + VmisProperties.GovProps gov = props.getGov(); + int connectTimeout = gov.getConnectTimeoutMillis(); + int readTimeout = gov.getReadTimeoutMillis(); + + RequestConfig requestConfig = RequestConfig.custom() + .setConnectTimeout(connectTimeout, java.util.concurrent.TimeUnit.MILLISECONDS) + .setResponseTimeout(readTimeout, java.util.concurrent.TimeUnit.MILLISECONDS) + .build(); + + PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); + cm.setMaxTotal(100); + cm.setDefaultMaxPerRoute(20); + + CloseableHttpClient httpClient = HttpClientBuilder.create() + .setDefaultRequestConfig(requestConfig) + .setConnectionManager(cm) + .build(); + + HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); + return builder + .requestFactory(() -> requestFactory) + .setConnectTimeout(Duration.ofMillis(connectTimeout)) + .setReadTimeout(Duration.ofMillis(readTimeout)) + .build(); + } +} diff --git a/src/main/java/com/vmis/interfaceapp/config/OpenApiConfig.java b/src/main/java/com/vmis/interfaceapp/config/OpenApiConfig.java new file mode 100644 index 0000000..43fd1c3 --- /dev/null +++ b/src/main/java/com/vmis/interfaceapp/config/OpenApiConfig.java @@ -0,0 +1,27 @@ +package com.vmis.interfaceapp.config; + +import io.swagger.v3.oas.models.ExternalDocumentation; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenApiConfig { + + @Bean + public OpenAPI vmisOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("VMIS Interface API") + .description("시군구연계 자동차 정보 인터페이스 API (자망연계)") + .version("v0.1.0") + .contact(new Contact().name("VMIS").email("support@example.com")) + .license(new License().name("Apache 2.0").url("https://www.apache.org/licenses/LICENSE-2.0.html"))) + .externalDocs(new ExternalDocumentation() + .description("Reference") + .url("")); + } +} diff --git a/src/main/java/com/vmis/interfaceapp/config/PropertiesConfig.java b/src/main/java/com/vmis/interfaceapp/config/PropertiesConfig.java new file mode 100644 index 0000000..c681595 --- /dev/null +++ b/src/main/java/com/vmis/interfaceapp/config/PropertiesConfig.java @@ -0,0 +1,10 @@ +package com.vmis.interfaceapp.config; + +import com.vmis.interfaceapp.config.properties.VmisProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties(VmisProperties.class) +public class PropertiesConfig { +} diff --git a/src/main/java/com/vmis/interfaceapp/config/properties/VmisProperties.java b/src/main/java/com/vmis/interfaceapp/config/properties/VmisProperties.java new file mode 100644 index 0000000..3042d2c --- /dev/null +++ b/src/main/java/com/vmis/interfaceapp/config/properties/VmisProperties.java @@ -0,0 +1,180 @@ +package com.vmis.interfaceapp.config.properties; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +@ConfigurationProperties(prefix = "vmis") +@Validated +public class VmisProperties { + + @NotNull + private SystemProps system = new SystemProps(); + @NotNull + private GpkiProps gpki = new GpkiProps(); + @NotNull + private GovProps gov = new GovProps(); + + public SystemProps getSystem() { return system; } + public void setSystem(SystemProps system) { this.system = system; } + public GpkiProps getGpki() { return gpki; } + public void setGpki(GpkiProps gpki) { this.gpki = gpki; } + public GovProps getGov() { return gov; } + public void setGov(GovProps gov) { this.gov = gov; } + + public static class SystemProps { + @NotBlank + private String infoSysId; + /** INFO_SYS_IP */ + private String infoSysIp; + /** 시군구코드 (SIGUNGU_CODE) */ + private String regionCode; + private String departmentCode; + // 담당자 정보 + private String chargerId; + private String chargerIp; + private String chargerNm; + + public String getInfoSysId() { return infoSysId; } + public void setInfoSysId(String infoSysId) { this.infoSysId = infoSysId; } + public String getInfoSysIp() { return infoSysIp; } + public void setInfoSysIp(String infoSysIp) { this.infoSysIp = infoSysIp; } + public String getRegionCode() { return regionCode; } + public void setRegionCode(String regionCode) { this.regionCode = regionCode; } + public String getDepartmentCode() { return departmentCode; } + public void setDepartmentCode(String departmentCode) { this.departmentCode = departmentCode; } + public String getChargerId() { return chargerId; } + public void setChargerId(String chargerId) { this.chargerId = chargerId; } + public String getChargerIp() { return chargerIp; } + public void setChargerIp(String chargerIp) { this.chargerIp = chargerIp; } + public String getChargerNm() { return chargerNm; } + public void setChargerNm(String chargerNm) { this.chargerNm = chargerNm; } + } + + public static class GpkiProps { + /** "Y" 또는 "N" */ + @NotBlank + private String enabled = "N"; + private boolean useSign = true; + @NotBlank + private String charset = "UTF-8"; + @NotBlank + private String certServerId; + @NotBlank + private String targetServerId; + // Optional advanced config for native GPKI util + private Boolean ldap; // null -> util default + private String gpkiLicPath; // e.g., C:/gpki2/gpkisecureweb/conf + private String certFilePath; // directory for target cert files when LDAP=false + private String envCertFilePathName; // ..._env.cer + private String envPrivateKeyFilePathName; // ..._env.key + private String envPrivateKeyPasswd; + private String sigCertFilePathName; // ..._sig.cer + private String sigPrivateKeyFilePathName; // ..._sig.key + private String sigPrivateKeyPasswd; + + public String getEnabled() { return enabled; } + public void setEnabled(String enabled) { this.enabled = enabled; } + public boolean isUseSign() { return useSign; } + public void setUseSign(boolean useSign) { this.useSign = useSign; } + public String getCharset() { return charset; } + public void setCharset(String charset) { this.charset = charset; } + public String getCertServerId() { return certServerId; } + public void setCertServerId(String certServerId) { this.certServerId = certServerId; } + public String getTargetServerId() { return targetServerId; } + public void setTargetServerId(String targetServerId) { this.targetServerId = targetServerId; } + public Boolean getLdap() { return ldap; } + public void setLdap(Boolean ldap) { this.ldap = ldap; } + public String getGpkiLicPath() { return gpkiLicPath; } + public void setGpkiLicPath(String gpkiLicPath) { this.gpkiLicPath = gpkiLicPath; } + public String getCertFilePath() { return certFilePath; } + public void setCertFilePath(String certFilePath) { this.certFilePath = certFilePath; } + public String getEnvCertFilePathName() { return envCertFilePathName; } + public void setEnvCertFilePathName(String envCertFilePathName) { this.envCertFilePathName = envCertFilePathName; } + public String getEnvPrivateKeyFilePathName() { return envPrivateKeyFilePathName; } + public void setEnvPrivateKeyFilePathName(String envPrivateKeyFilePathName) { this.envPrivateKeyFilePathName = envPrivateKeyFilePathName; } + public String getEnvPrivateKeyPasswd() { return envPrivateKeyPasswd; } + public void setEnvPrivateKeyPasswd(String envPrivateKeyPasswd) { this.envPrivateKeyPasswd = envPrivateKeyPasswd; } + public String getSigCertFilePathName() { return sigCertFilePathName; } + public void setSigCertFilePathName(String sigCertFilePathName) { this.sigCertFilePathName = sigCertFilePathName; } + public String getSigPrivateKeyFilePathName() { return sigPrivateKeyFilePathName; } + public void setSigPrivateKeyFilePathName(String sigPrivateKeyFilePathName) { this.sigPrivateKeyFilePathName = sigPrivateKeyFilePathName; } + public String getSigPrivateKeyPasswd() { return sigPrivateKeyPasswd; } + public void setSigPrivateKeyPasswd(String sigPrivateKeyPasswd) { this.sigPrivateKeyPasswd = sigPrivateKeyPasswd; } + + public boolean isEnabledFlag() { return "Y".equalsIgnoreCase(enabled); } + } + + public static class GovProps { + @NotBlank + private String scheme = "http"; + @NotBlank + private String host; + @NotBlank + private String basePath; + private int connectTimeoutMillis = 5000; + private int readTimeoutMillis = 10000; + @NotNull + private Services services = new Services(); + + public String getScheme() { return scheme; } + public void setScheme(String scheme) { this.scheme = scheme; } + public String getHost() { return host; } + public void setHost(String host) { this.host = host; } + public String getBasePath() { return basePath; } + public void setBasePath(String basePath) { this.basePath = basePath; } + public int getConnectTimeoutMillis() { return connectTimeoutMillis; } + public void setConnectTimeoutMillis(int connectTimeoutMillis) { this.connectTimeoutMillis = connectTimeoutMillis; } + public int getReadTimeoutMillis() { return readTimeoutMillis; } + public void setReadTimeoutMillis(int readTimeoutMillis) { this.readTimeoutMillis = readTimeoutMillis; } + public Services getServices() { return services; } + public void setServices(Services services) { this.services = services; } + + public String buildServiceUrl(String path) { + StringBuilder sb = new StringBuilder(); + sb.append(scheme).append("://").append(host); + if (basePath != null && !basePath.isEmpty()) { + if (!basePath.startsWith("/")) sb.append('/'); + sb.append(basePath); + } + if (path != null && !path.isEmpty()) { + if (!path.startsWith("/")) sb.append('/'); + sb.append(path); + } + return sb.toString(); + } + + public static class Services { + @NotNull + private Service basic = new Service(); + @NotNull + private Service ledger = new Service(); + + public Service getBasic() { return basic; } + public void setBasic(Service basic) { this.basic = basic; } + public Service getLedger() { return ledger; } + public void setLedger(Service ledger) { this.ledger = ledger; } + } + + public static class Service { + @NotBlank + private String path; + @NotBlank + private String cntcInfoCode; + @NotBlank + private String apiKey; + @NotBlank + private String cvmisApikey; + + public String getPath() { return path; } + public void setPath(String path) { this.path = path; } + public String getCntcInfoCode() { return cntcInfoCode; } + public void setCntcInfoCode(String cntcInfoCode) { this.cntcInfoCode = cntcInfoCode; } + public String getApiKey() { return apiKey; } + public void setApiKey(String apiKey) { this.apiKey = apiKey; } + public String getCvmisApikey() { return cvmisApikey; } + public void setCvmisApikey(String cvmisApikey) { this.cvmisApikey = cvmisApikey; } + } + } +} diff --git a/src/main/java/com/vmis/interfaceapp/controller/VehicleInterfaceController.java b/src/main/java/com/vmis/interfaceapp/controller/VehicleInterfaceController.java new file mode 100644 index 0000000..a60b47c --- /dev/null +++ b/src/main/java/com/vmis/interfaceapp/controller/VehicleInterfaceController.java @@ -0,0 +1,211 @@ +package com.vmis.interfaceapp.controller; + +import com.vmis.interfaceapp.client.GovernmentApiClient; +import com.vmis.interfaceapp.model.basic.BasicRequest; +import com.vmis.interfaceapp.model.basic.BasicResponse; +import com.vmis.interfaceapp.model.common.Envelope; +import com.vmis.interfaceapp.model.ledger.LedgerRequest; +import com.vmis.interfaceapp.model.ledger.LedgerResponse; +import com.vmis.interfaceapp.service.RequestEnricher; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +/** + * 자동차 정보 연계 REST 컨트롤러 + * + *

이 컨트롤러는 시군구연계 자동차 정보 조회 API의 진입점(Entry Point)을 제공합니다. + * 클라이언트로부터 HTTP 요청을 받아 정부 시스템과의 통신을 중계하는 역할을 담당합니다.

+ * + *

주요 기능:

+ *
    + *
  • 자동차 기본사항 조회 API 제공
  • + *
  • 자동차 등록원부(갑) 조회 API 제공
  • + *
  • 요청 데이터를 Envelope로 감싸서 정부 시스템으로 전달
  • + *
  • 정부 시스템의 응답을 클라이언트에게 반환
  • + *
+ * + *

API 경로:

+ *
    + *
  • 기본 경로: /api/v1/vehicles
  • + *
  • 자동차 기본사항 조회: POST /api/v1/vehicles/basic
  • + *
  • 자동차 등록원부 조회: POST /api/v1/vehicles/ledger
  • + *
+ * + *

아키텍처 설계:

+ *
    + *
  • 컨트롤러는 얇은(Thin) 레이어로 설계되어 비즈니스 로직을 포함하지 않음
  • + *
  • 모든 실제 처리는 {@link GovernmentApiClient}에 위임
  • + *
  • Swagger/OpenAPI 문서 자동 생성을 위한 어노테이션 포함
  • + *
+ * + * @see GovernmentApiClient + * @see Envelope + */ +@RestController +@RequestMapping("/api/v1/vehicles") +@Tag(name = "Vehicle Interfaces", description = "시군구연계 자동차 정보 연계 API") +public class VehicleInterfaceController { + + private final RequestEnricher enricher; + + /** + * 정부 API 클라이언트 + * + *

정부 시스템과의 실제 통신을 담당하는 클라이언트 객체입니다. + * 이 클라이언트는 다음 기능들을 포함합니다:

+ *
    + *
  • HTTP 요청/응답 처리
  • + *
  • GPKI 암호화/복호화
  • + *
  • 헤더 구성 및 관리
  • + *
  • 에러 처리 및 로깅
  • + *
+ * + *

생성자 주입(Constructor Injection)을 통해 의존성이 주입되며, + * final 키워드로 선언되어 불변성을 보장합니다.

+ */ + private final GovernmentApiClient govClient; + + /** + * 생성자를 통한 의존성 주입 + * + *

Spring Framework의 생성자 주입 방식을 사용합니다. + * 이 방식은 다음과 같은 장점을 제공합니다:

+ *
    + *
  • 필수 의존성을 명확하게 표현
  • + *
  • 불변성 보장 (final 필드 사용 가능)
  • + *
  • 단위 테스트 시 Mock 객체 주입 용이
  • + *
  • 순환 참조 방지
  • + *
+ * + * @param govClient 정부 API와 통신하는 클라이언트 객체 + */ + public VehicleInterfaceController(GovernmentApiClient govClient, RequestEnricher enricher) { + this.govClient = govClient; + this.enricher = enricher; + } + + /** + * 자동차 기본사항 조회 API + * + *

차량번호, 소유자 정보 등 자동차의 기본적인 정보를 조회하는 API입니다.

+ * + *

요청 처리 흐름:

+ *
    + *
  1. 클라이언트로부터 JSON 형식의 요청 수신
  2. + *
  3. 요청 바디가 Envelope<BasicRequest> 객체로 역직렬화됨
  4. + *
  5. Spring의 @RequestBody 어노테이션이 자동으로 JSON을 객체로 변환
  6. + *
  7. 변환된 요청 객체를 GovernmentApiClient로 전달
  8. + *
  9. GovernmentApiClient가 정부 시스템과 통신 수행
  10. + *
  11. 응답 데이터를 Envelope<BasicResponse>로 감싸서 반환
  12. + *
  13. Spring이 자동으로 응답 객체를 JSON으로 직렬화하여 클라이언트에게 전송
  14. + *
+ * + *

HTTP 메서드 및 경로:

+ *
    + *
  • 메서드: POST
  • + *
  • 경로: /api/v1/vehicles/basic
  • + *
  • Content-Type: application/json (요청)
  • + *
  • Accept: application/json (응답)
  • + *
+ * + *

데이터 구조:

+ *
    + *
  • Envelope: 공통 헤더 정보(전문 ID, 전송 시각 등)와 실제 데이터를 감싸는 래퍼 객체
  • + *
  • BasicRequest: 자동차 기본사항 조회를 위한 요청 데이터 (차량번호 등)
  • + *
  • BasicResponse: 자동차 기본사항 조회 결과 데이터 (소유자, 차종, 연식 등)
  • + *
+ * + *

에러 처리:

+ *
    + *
  • JSON 파싱 오류 시 400 Bad Request 반환 (Spring 자동 처리)
  • + *
  • 정부 API 호출 실패 시 해당 HTTP 상태 코드 그대로 반환
  • + *
  • 네트워크 오류나 기타 예외 발생 시 RuntimeException으로 래핑되어 처리
  • + *
+ * + * @param envelope 자동차 기본사항 조회 요청을 담은 Envelope 객체 공통 헤더(header)와 실제 요청 데이터(data)로 구성됨 + * @return ResponseEntity<Envelope<BasicResponse>> 조회 결과를 담은 응답 객체 HTTP 상태 코드, 헤더, 응답 바디를 포함 + */ + @PostMapping(value = "/basic", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "자동차기본사항조회", description = "시군구연계 자동차기본사항조회 인터페이스. 요청 바디를 모델로 받아 정부시스템으로 전달합니다.") + public ResponseEntity> basic( + @org.springframework.web.bind.annotation.RequestBody Envelope envelope + ) { + // YAML 설정값으로 공통 필드 자동 채움 + enricher.enrichBasic(envelope); + // 실제 처리는 GovernmentApiClient에 위임 + return govClient.callBasic(envelope); + } + + /** + * 자동차 등록원부(갑) 조회 API + * + *

자동차 등록원부의 갑지에 기재된 상세한 등록 정보를 조회하는 API입니다. + * 등록원부는 자동차의 법적 소유관계와 등록사항을 증명하는 공적 장부입니다.

+ * + *

요청 처리 흐름:

+ *
    + *
  1. 클라이언트로부터 JSON 형식의 요청 수신
  2. + *
  3. 요청 바디가 Envelope<LedgerRequest> 객체로 역직렬화됨
  4. + *
  5. Spring의 @RequestBody 어노테이션이 자동으로 JSON을 객체로 변환
  6. + *
  7. 변환된 요청 객체를 GovernmentApiClient로 전달
  8. + *
  9. GovernmentApiClient가 정부 시스템과 통신 수행
  10. + *
  11. 응답 데이터를 Envelope<LedgerResponse>로 감싸서 반환
  12. + *
  13. Spring이 자동으로 응답 객체를 JSON으로 직렬화하여 클라이언트에게 전송
  14. + *
+ * + *

HTTP 메서드 및 경로:

+ *
    + *
  • 메서드: POST
  • + *
  • 경로: /api/v1/vehicles/ledger
  • + *
  • Content-Type: application/json (요청)
  • + *
  • Accept: application/json (응답)
  • + *
+ * + *

데이터 구조:

+ *
    + *
  • Envelope: 공통 헤더 정보(전문 ID, 전송 시각 등)와 실제 데이터를 감싸는 래퍼 객체
  • + *
  • LedgerRequest: 자동차 등록원부 조회를 위한 요청 데이터 (차량번호, 소유자 정보 등)
  • + *
  • LedgerResponse: 자동차 등록원부 조회 결과 데이터 (상세한 등록사항, 소유권 이력 등)
  • + *
+ * + *

등록원부(갑)에 포함되는 정보:

+ *
    + *
  • 차량 기본 정보 (차명, 차종, 용도 등)
  • + *
  • 소유자 정보 (성명, 주소, 주민등록번호 등)
  • + *
  • 등록 이력 (최초 등록일, 변경 이력 등)
  • + *
  • 저당권 및 압류 정보
  • + *
  • 기타 법적 권리관계
  • + *
+ * + *

에러 처리:

+ *
    + *
  • JSON 파싱 오류 시 400 Bad Request 반환 (Spring 자동 처리)
  • + *
  • 정부 API 호출 실패 시 해당 HTTP 상태 코드 그대로 반환
  • + *
  • 네트워크 오류나 기타 예외 발생 시 RuntimeException으로 래핑되어 처리
  • + *
  • 권한 부족 시 403 Forbidden 또는 401 Unauthorized 반환 가능
  • + *
+ * + *

보안 및 개인정보:

+ *
    + *
  • 민감한 개인정보를 포함하므로 적절한 인증/인가 필요
  • + *
  • GPKI 암호화를 통한 데이터 보안 강화
  • + *
  • 로그 기록 시 개인정보 마스킹 필요
  • + *
+ * + * @param envelope 자동차 등록원부 조회 요청을 담은 Envelope 객체 공통 헤더(header)와 실제 요청 데이터(data)로 구성됨 + * @return ResponseEntity<Envelope<LedgerResponse>> 등록원부 조회 결과를 담은 응답 객체 HTTP 상태 코드, 헤더, 응답 바디를 포함 + */ + @PostMapping(value = "/ledger", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + @Operation(summary = "자동차등록원부(갑)", description = "시군구연계 자동차등록원부(갑) 인터페이스. 요청 바디를 모델로 받아 정부시스템으로 전달합니다.") + public ResponseEntity> ledger( + @org.springframework.web.bind.annotation.RequestBody Envelope envelope + ) { + // YAML 설정값으로 공통 필드 자동 채움 + enricher.enrichLedger(envelope); + // 실제 처리는 GovernmentApiClient에 위임 + return govClient.callLedger(envelope); + } +} diff --git a/src/main/java/com/vmis/interfaceapp/gpki/GpkiService.java b/src/main/java/com/vmis/interfaceapp/gpki/GpkiService.java new file mode 100644 index 0000000..3320a72 --- /dev/null +++ b/src/main/java/com/vmis/interfaceapp/gpki/GpkiService.java @@ -0,0 +1,7 @@ +package com.vmis.interfaceapp.gpki; + +public interface GpkiService { + String encrypt(String plain) throws Exception; + String decrypt(String cipher) throws Exception; + boolean isEnabled(); +} diff --git a/src/main/java/com/vmis/interfaceapp/gpki/NoopGpkiService.java b/src/main/java/com/vmis/interfaceapp/gpki/NoopGpkiService.java new file mode 100644 index 0000000..c40351f --- /dev/null +++ b/src/main/java/com/vmis/interfaceapp/gpki/NoopGpkiService.java @@ -0,0 +1,18 @@ +package com.vmis.interfaceapp.gpki; + +public class NoopGpkiService implements GpkiService { + @Override + public String encrypt(String plain) { + return plain; + } + + @Override + public String decrypt(String cipher) { + return cipher; + } + + @Override + public boolean isEnabled() { + return false; + } +} diff --git a/src/main/java/com/vmis/interfaceapp/gpki/RealGpkiService.java b/src/main/java/com/vmis/interfaceapp/gpki/RealGpkiService.java new file mode 100644 index 0000000..78bcec3 --- /dev/null +++ b/src/main/java/com/vmis/interfaceapp/gpki/RealGpkiService.java @@ -0,0 +1,41 @@ +package com.vmis.interfaceapp.gpki; + +import com.vmis.interfaceapp.config.properties.VmisProperties; +import com.vmis.interfaceapp.util.GpkiCryptoUtil; + +/** + * Real GPKI service backed by native GPKI JNI via legacy NewGpkiUtil wrapper. + * Uses YAML-configured paths and options in {@link VmisProperties.GpkiProps}. + */ +public class RealGpkiService implements GpkiService { + + private final VmisProperties props; + private final GpkiCryptoUtil crypto; + + public RealGpkiService(VmisProperties props) { + this.props = props; + try { + this.crypto = GpkiCryptoUtil.from(props.getGpki()); + } catch (Exception e) { + throw new IllegalStateException("Failed to initialize GPKI (JNI) util. Check YAML paths/passwords and license.", e); + } + } + + @Override + public String encrypt(String plain) throws Exception { + String charset = props.getGpki().getCharset(); + String targetId = props.getGpki().getTargetServerId(); + return crypto.encryptToBase64(plain, targetId, charset); + } + + @Override + public String decrypt(String cipher) throws Exception { + String charset = props.getGpki().getCharset(); + return crypto.decryptFromBase64(cipher, charset); + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/src/main/java/com/vmis/interfaceapp/model/basic/BasicRequest.java b/src/main/java/com/vmis/interfaceapp/model/basic/BasicRequest.java new file mode 100644 index 0000000..4eb2ac8 --- /dev/null +++ b/src/main/java/com/vmis/interfaceapp/model/basic/BasicRequest.java @@ -0,0 +1,88 @@ +package com.vmis.interfaceapp.model.basic; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Getter +@Setter +@Schema(description = "자동차기본사항조회 요청 항목") +public class BasicRequest { + + // 본문 공통 메타 + @Schema(description = "정보시스템ID") + @JsonProperty("INFO_SYS_ID") + private String infoSysId; + + @Schema(description = "정보시스템IP") + @JsonProperty("INFO_SYS_IP") + private String infoSysIp; + + @Schema(description = "시군구코드") + @JsonProperty("SIGUNGU_CODE") + private String sigunguCode; + + // 서비스별 필드 + @Schema(description = "연계정보코드", example = "AC1_FD11_01") + @JsonProperty("CNTC_INFO_CODE") + private String cntcInfoCode; + + @Schema(description = "담당자ID") + @JsonProperty("CHARGER_ID") + private String chargerId; + + @Schema(description = "담당자IP") + @JsonProperty("CHARGER_IP") + private String chargerIp; + + @Schema(description = "담당자명(사용자)") + @JsonProperty("CHARGER_NM") + private String chargerNm; + + @Schema(description = "부과기준일", example = "20250101") + @JsonProperty("LEVY_STDDE") + private String levyStdde; + + @Schema(description = "조회구분코드") + @JsonProperty("INQIRE_SE_CODE") + private String inqireSeCode; + + @Schema(description = "자동차등록번호", example = "12가3456") + @JsonProperty("VHRNO") + private String vhrno; + + @Schema(description = "차대번호", example = "KMHAB812345678901") + @JsonProperty("VIN") + private String vin; + + // 추가 항목 (명세 샘플 기준) + @Schema(description = "개인정보공개", example = "Y") + @JsonProperty("ONES_INFORMATION_OPEN") + private String onesInformationOpen; + + @Schema(description = "민원인성명") + @JsonProperty("CPTTR_NM") + private String cpttrNm; + + @Schema(description = "민원인주민번호") + @JsonProperty("CPTTR_IHIDNUM") + @Size(max = 13) + private String cpttrIhidnum; + + @Schema(description = "민원인법정동코드") + @JsonProperty("CPTTR_LEGALDONG_CODE") + private String cpttrLegaldongCode; + + @Schema(description = "경로구분코드") + @JsonProperty("ROUTE_SE_CODE") + private String routeSeCode; + + @Schema(description = "내역표시") + @JsonProperty("DETAIL_EXPRESSION") + private String detailExpression; + +} diff --git a/src/main/java/com/vmis/interfaceapp/model/basic/BasicResponse.java b/src/main/java/com/vmis/interfaceapp/model/basic/BasicResponse.java new file mode 100644 index 0000000..c83dc0f --- /dev/null +++ b/src/main/java/com/vmis/interfaceapp/model/basic/BasicResponse.java @@ -0,0 +1,121 @@ +package com.vmis.interfaceapp.model.basic; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@Schema(description = "자동차기본사항조회 응답 모델") +@Getter +@Setter +public class BasicResponse { + + @JsonProperty("CNTC_RESULT_CODE") + private String cntcResultCode; + + @JsonProperty("CNTC_RESULT_DTLS") + private String cntcResultDtls; + + @JsonProperty("record") + private List record; + + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + @Schema(description = "기본사항 record 항목") + @Getter + @Setter + public static class Record { + @JsonProperty("PRYE") private String prye; + @JsonProperty("REGIST_DE") private String registDe; + @JsonProperty("ERSR_REGIST_SE_CODE") private String ersrRegistSeCode; + @JsonProperty("ERSR_REGIST_SE_NM") private String ersrRegistSeNm; + @JsonProperty("ERSR_REGIST_DE") private String ersrRegistDe; + @JsonProperty("REGIST_DETAIL_CODE") private String registDetailCode; + @JsonProperty("DSPLVL") private String dsplvl; + @JsonProperty("USE_STRNGHLD_LEGALDONG_CODE") private String useStrnghldLegaldongCode; + @JsonProperty("USE_STRNGHLD_ADSTRD_CODE") private String useStrnghldAdstrdCode; + @JsonProperty("USE_STRNGHLD_MNTN") private String useStrnghldMntn; + @JsonProperty("USE_STRNGHLD_LNBR") private String useStrnghldLnbr; + @JsonProperty("USE_STRNGHLD_HO") private String useStrnghldHo; + @JsonProperty("USE_STRNGHLD_ADRES_NM") private String useStrnghldAdresNm; + @JsonProperty("USE_STRNGHLD_ROAD_NM_CODE") private String useStrnghldRoadNmCode; + @JsonProperty("USGSRHLD_UNDGRND_BULD_SE_CODE") private String usgsrhldUndgrndBuldSeCode; + @JsonProperty("USE_STRNGHLD_BULD_MAIN_NO") private String useStrnghldBuldMainNo; + @JsonProperty("USE_STRNGHLD_BULD_SUB_NO") private String useStrnghldBuldSubNo; + @JsonProperty("USGSRHLD_ADRES_FULL") private String usgsrhldAdresFull; + @JsonProperty("MBER_SE_CODE") private String mberSeCode; + @JsonProperty("MBER_NM") private String mberNm; + @JsonProperty("MBER_SE_NO") private String mberSeNo; + @JsonProperty("TELNO") private String telno; + @JsonProperty("OWNER_LEGALDONG_CODE") private String ownerLegaldongCode; + @JsonProperty("OWNER_ADSTRD_CODE") private String ownerAdstrdCode; + @JsonProperty("OWNER_MNTN") private String ownerMntn; + @JsonProperty("OWNER_LNBR") private String ownerLnbr; + @JsonProperty("OWNER_HO") private String ownerHo; + @JsonProperty("OWNER_ADRES_NM") private String ownerAdresNm; + @JsonProperty("OWNER_ROAD_NM_CODE") private String ownerRoadNmCode; + @JsonProperty("OWNER_UNDGRND_BULD_SE_CODE") private String ownerUndgrndBuldSeCode; + @JsonProperty("OWNER_BULD_MAIN_NO") private String ownerBuldMainNo; + @JsonProperty("OWNER_BULD_SUB_NO") private String ownerBuldSubNo; + @JsonProperty("OWNER_ADRES_FULL") private String ownerAdresFull; + @JsonProperty("AFTR_VHRNO") private String aftrVhrno; + @JsonProperty("USE_FUEL_CODE") private String useFuelCode; + @JsonProperty("PRPOS_SE_CODE") private String prposSeCode; + @JsonProperty("MTRS_FOM_NM") private String mtrsFomNm; + @JsonProperty("FRNT_VHRNO") private String frntVhrno; + @JsonProperty("VHRNO") private String vhrno; + @JsonProperty("VIN") private String vin; + @JsonProperty("CNM") private String cnm; + @JsonProperty("VHCLE_TOT_WT") private String vhcleTotWt; + @JsonProperty("CAAG_ENDDE") private String caagEndde; + @JsonProperty("CHANGE_DE") private String changeDe; + @JsonProperty("VHCTY_ASORT_CODE") private String vhctyAsortCode; + @JsonProperty("VHCTY_TY_CODE") private String vhctyTyCode; + @JsonProperty("VHCTY_SE_CODE") private String vhctySeCode; + @JsonProperty("MXMM_LDG") private String mxmmLdg; + @JsonProperty("VHCTY_ASORT_NM") private String vhctyAsortNm; + @JsonProperty("VHCTY_TY_NM") private String vhctyTyNm; + @JsonProperty("VHCTY_SE_NM") private String vhctySeNm; + @JsonProperty("FRST_REGIST_DE") private String frstRegistDe; + @JsonProperty("FOM_NM") private String fomNm; + @JsonProperty("ACQS_DE") private String acqsDe; + @JsonProperty("ACQS_END_DE") private String acqsEndDe; + @JsonProperty("YBL_MD") private String yblMd; + @JsonProperty("TRANSR_REGIST_DE") private String transrRegistDe; + @JsonProperty("SPCF_REGIST_STTUS_CODE") private String spcfRegistSttusCode; + @JsonProperty("COLOR_NM") private String colorNm; + @JsonProperty("MRTG_CO") private String mrtgCo; + @JsonProperty("SEIZR_CO") private String seizrCo; + @JsonProperty("STMD_CO") private String stmdCo; + @JsonProperty("NMPL_CSDY_AT") private String nmplCsdyAt; + @JsonProperty("NMPL_CSDY_REMNR_DE") private String nmplCsdyRemnrDe; + @JsonProperty("ORIGIN_SE_CODE") private String originSeCode; + @JsonProperty("NMPL_STNDRD_CODE") private String nmplStndrdCode; + @JsonProperty("ACQS_AMOUNT") private String acqsAmount; + @JsonProperty("INSPT_VALID_PD_BGNDE") private String insptValidPdBgnde; + @JsonProperty("INSPT_VALID_PD_ENDDE") private String insptValidPdEndde; + @JsonProperty("USE_STRNGHLD_GRC_CODE") private String useStrnghldGrcCode; + @JsonProperty("TKCAR_PSCAP_CO") private String tkcarPscapCo; + @JsonProperty("SPMNNO") private String spmnno; + @JsonProperty("TRVL_DSTNC") private String trvlDstnc; + @JsonProperty("FRST_REGIST_RQRCNO") private String frstRegistRqrcno; + @JsonProperty("VLNT_ERSR_PRVNTC_NTICE_DE") private String vlntErsrPrvntcNticeDe; + @JsonProperty("REGIST_INSTT_NM") private String registInsttNm; + @JsonProperty("PROCESS_IMPRTY_RESN_CODE") private String processImprtyResnCode; + @JsonProperty("PROCESS_IMPRTY_RESN_DTLS") private String processImprtyResnDtls; + @JsonProperty("CBD_LT") private String cbdLt; + @JsonProperty("CBD_BT") private String cbdBt; + @JsonProperty("CBD_HG") private String cbdHg; + @JsonProperty("FRST_MXMM_LDG") private String frstMxmmLdg; + @JsonProperty("FUEL_CNSMP_RT") private String fuelCnsmpRt; + @JsonProperty("ELCTY_CMPND_FUEL_CNSMP_RT") private String elctyCmpndFuelCnsmpRt; + + } +} diff --git a/src/main/java/com/vmis/interfaceapp/model/common/Envelope.java b/src/main/java/com/vmis/interfaceapp/model/common/Envelope.java new file mode 100644 index 0000000..12f5586 --- /dev/null +++ b/src/main/java/com/vmis/interfaceapp/model/common/Envelope.java @@ -0,0 +1,32 @@ +package com.vmis.interfaceapp.model.common; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +import java.util.ArrayList; +import java.util.List; + +/** + * 공통 래퍼: { "data": [ ... ] } + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +@Getter +@Setter +public class Envelope { + + @JsonProperty("data") + private List data = new ArrayList<>(); + + public Envelope() {} + + public Envelope(T single) { + if (single != null) this.data.add(single); + } + + public Envelope(List data) { + this.data = data; + } + +} diff --git a/src/main/java/com/vmis/interfaceapp/model/ledger/LedgerRequest.java b/src/main/java/com/vmis/interfaceapp/model/ledger/LedgerRequest.java new file mode 100644 index 0000000..af66d45 --- /dev/null +++ b/src/main/java/com/vmis/interfaceapp/model/ledger/LedgerRequest.java @@ -0,0 +1,79 @@ +package com.vmis.interfaceapp.model.ledger; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Size; +import lombok.Getter; +import lombok.Setter; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Schema(description = "자동차등록원부(갑) 요청 항목") +@Getter +@Setter +public class LedgerRequest { + + // 본문 공통 메타 + @Schema(description = "정보시스템ID") + @JsonProperty("INFO_SYS_ID") + private String infoSysId; + + @Schema(description = "정보시스템IP") + @JsonProperty("INFO_SYS_IP") + private String infoSysIp; + + @Schema(description = "시군구코드") + @JsonProperty("SIGUNGU_CODE") + private String sigunguCode; + + // 서비스별 필드 + @Schema(description = "연계정보코드", example = "AC1_FD11_02") + @JsonProperty("CNTC_INFO_CODE") + private String cntcInfoCode; + + @Schema(description = "담당자ID") + @JsonProperty("CHARGER_ID") + private String chargerId; + + @Schema(description = "담당자IP") + @JsonProperty("CHARGER_IP") + private String chargerIp; + + @Schema(description = "담당자명(사용자)") + @JsonProperty("CHARGER_NM") + private String chargerNm; + + @Schema(description = "자동차등록번호") + @JsonProperty("VHRNO") + private String vhrno; + + @Schema(description = "개인정보공개") + @JsonProperty("ONES_INFORMATION_OPEN") + private String onesInformationOpen; + + @Schema(description = "민원인성명") + @JsonProperty("CPTTR_NM") + private String cpttrNm; + + @Schema(description = "민원인주민번호") + @JsonProperty("CPTTR_IHIDNUM") + @Size(max = 13) + private String cpttrIhidnum; + + @Schema(description = "민원인법정동코드") + @JsonProperty("CPTTR_LEGALDONG_CODE") + private String cpttrLegaldongCode; + + @Schema(description = "경로구분코드") + @JsonProperty("ROUTE_SE_CODE") + private String routeSeCode; + + @Schema(description = "내역표시") + @JsonProperty("DETAIL_EXPRESSION") + private String detailExpression; + + @Schema(description = "조회구분코드") + @JsonProperty("INQIRE_SE_CODE") + private String inqireSeCode; + +} diff --git a/src/main/java/com/vmis/interfaceapp/model/ledger/LedgerResponse.java b/src/main/java/com/vmis/interfaceapp/model/ledger/LedgerResponse.java new file mode 100644 index 0000000..3a49a0f --- /dev/null +++ b/src/main/java/com/vmis/interfaceapp/model/ledger/LedgerResponse.java @@ -0,0 +1,250 @@ +package com.vmis.interfaceapp.model.ledger; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +@Schema(description = "자동차등록원부(갑) 응답 모델") +@Getter +@Setter +public class LedgerResponse { + + @JsonProperty("CNTC_RESULT_CODE") + private String cntcResultCode; + + @JsonProperty("CNTC_RESULT_DTLS") + private String cntcResultDtls; + + @JsonProperty("LEDGER_GROUP_NO") + private String ledgerGroupNo; + + @JsonProperty("LEDGER_INDVDLZ_NO") + private String ledgerIndvdlzNo; + + @JsonProperty("VHMNO") + private String vhmno; + + @JsonProperty("VHRNO") + private String vhrno; + + @JsonProperty("VIN") + private String vin; + + @JsonProperty("VHCTY_ASORT_CODE") + private String vhctyAsortCode; + + @JsonProperty("VHCTY_ASORT_NM") + private String vhctyAsortNm; + + @JsonProperty("CNM") + private String cnm; + + @JsonProperty("COLOR_CODE") + private String colorCode; + + @JsonProperty("COLOR_NM") + private String colorNm; + + @JsonProperty("NMPL_STNDRD_CODE") + private String nmplStndrdCode; + + @JsonProperty("NMPL_STNDRD_NM") + private String nmplStndrdNm; + + @JsonProperty("PRPOS_SE_CODE") + private String prposSeCode; + + @JsonProperty("PRPOS_SE_NM") + private String prposSeNm; + + @JsonProperty("MTRS_FOM_NM") + private String mtrsFomNm; + + @JsonProperty("FOM_NM") + private String fomNm; + + @JsonProperty("ACQS_AMOUNT") + private String acqsAmount; + + @JsonProperty("REGIST_DETAIL_CODE") + private String registDetailCode; + + @JsonProperty("REGIST_DETAIL_NM") + private String registDetailNm; + + @JsonProperty("FRST_REGIST_DE") + private String frstRegistDe; + + @JsonProperty("CAAG_ENDDE") + private String caagEndde; + + @JsonProperty("PRYE") + private String prye; + + @JsonProperty("SPMNNO1") + private String spmnno1; + + @JsonProperty("SPMNNO2") + private String spmnno2; + + @JsonProperty("YBL_MD") + private String yblMd; + + @JsonProperty("TRVL_DSTNC") + private String trvlDstnc; + + @JsonProperty("INSPT_VALID_PD_BGNDE") + private String insptValidPdBgnde; + + @JsonProperty("INSPT_VALID_PD_ENDDE") + private String insptValidPdEndde; + + @JsonProperty("CHCK_VALID_PD_BGNDE") + private String chckValidPdBgnde; + + @JsonProperty("CHCK_VALID_PD_ENDDE") + private String chckValidPdEndde; + + @JsonProperty("REGIST_REQST_SE_NM") + private String registReqstSeNm; + + @JsonProperty("FRST_REGIST_RQRCNO") + private String frstRegistRqrcno; + + @JsonProperty("NMPL_CSDY_REMNR_DE") + private String nmplCsdyRemnrDe; + + @JsonProperty("NMPL_CSDY_AT") + private String nmplCsdyAt; + + @JsonProperty("BSS_USE_PD") + private String bssUsePd; + + @JsonProperty("OCTHT_ERSR_PRVNTC_NTICE_DE") + private String octhtErsrPrvntcNticeDe; + + @JsonProperty("ERSR_REGIST_DE") + private String ersrRegistDe; + + @JsonProperty("ERSR_REGIST_SE_CODE") + private String ersrRegistSeCode; + + @JsonProperty("ERSR_REGIST_SE_NM") + private String ersrRegistSeNm; + + @JsonProperty("MRTGCNT") + private String mrtgcnt; + + @JsonProperty("VHCLECNT") + private String vhclecnt; + + @JsonProperty("STMDCNT") + private String stmdcnt; + + @JsonProperty("ADRES1") + private String adres1; + + @JsonProperty("ADRES_NM1") + private String adresNm1; + + @JsonProperty("ADRES") + private String adres; + + @JsonProperty("ADRES_NM") + private String adresNm; + + @JsonProperty("INDVDL_BSNM_AT") + private String indvdlBsnmAt; + + @JsonProperty("TELNO") + private String telno; + + @JsonProperty("MBER_NM") + private String mberNm; + + @JsonProperty("MBER_SE_CODE") + private String mberSeCode; + + @JsonProperty("MBER_SE_NO") + private String mberSeNo; + + @JsonProperty("TAXXMPT_TRGTER_SE_CODE") + private String taxxmptTrgterSeCode; + + @JsonProperty("TAXXMPT_TRGTER_SE_CODE_NM") + private String taxxmptTrgterSeCodeNm; + + @JsonProperty("CNT_MATTER") + private String cntMatter; + + @JsonProperty("EMD_NM") + private String emdNm; + + @JsonProperty("PRVNTCCNT") + private String prvntccnt; + + @JsonProperty("XPORT_FLFL_AT_STTEMNT_DE") + private String xportFlflAtSttemntDe; + + @JsonProperty("PARTN_RQRCNO") + private String partnRqrcno; + + @JsonProperty("record") + private List record; + + + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + @Schema(description = "원부 변경내역 record") + @Getter + @Setter + public static class Record { + @JsonProperty("MAINCHK") private String mainchk; + @JsonProperty("CHANGE_JOB_SE_CODE") private String changeJobSeCode; + @JsonProperty("MAINNO") private String mainno; + @JsonProperty("SUBNO") private String subno; + @JsonProperty("DTLS") private String dtls; + @JsonProperty("RQRCNO") private String rqrcno; + @JsonProperty("VHMNO") private String vhmno; + @JsonProperty("LEDGER_GROUP_NO") private String ledgerGroupNo; + @JsonProperty("LEDGER_INDVDLZ_NO") private String ledgerIndvdlzNo; + @JsonProperty("GUBUN_NM") private String gubunNm; + @JsonProperty("CHANGE_DE") private String changeDe; + @JsonProperty("DETAIL_SN") private String detailSn; + @JsonProperty("FLAG") private String flag; + + public String getMainchk() { return mainchk; } + public void setMainchk(String mainchk) { this.mainchk = mainchk; } + public String getChangeJobSeCode() { return changeJobSeCode; } + public void setChangeJobSeCode(String changeJobSeCode) { this.changeJobSeCode = changeJobSeCode; } + public String getMainno() { return mainno; } + public void setMainno(String mainno) { this.mainno = mainno; } + public String getSubno() { return subno; } + public void setSubno(String subno) { this.subno = subno; } + public String getDtls() { return dtls; } + public void setDtls(String dtls) { this.dtls = dtls; } + public String getRqrcno() { return rqrcno; } + public void setRqrcno(String rqrcno) { this.rqrcno = rqrcno; } + public String getVhmno() { return vhmno; } + public void setVhmno(String vhmno) { this.vhmno = vhmno; } + public String getLedgerGroupNo() { return ledgerGroupNo; } + public void setLedgerGroupNo(String ledgerGroupNo) { this.ledgerGroupNo = ledgerGroupNo; } + public String getLedgerIndvdlzNo() { return ledgerIndvdlzNo; } + public void setLedgerIndvdlzNo(String ledgerIndvdlzNo) { this.ledgerIndvdlzNo = ledgerIndvdlzNo; } + public String getGubunNm() { return gubunNm; } + public void setGubunNm(String gubunNm) { this.gubunNm = gubunNm; } + public String getChangeDe() { return changeDe; } + public void setChangeDe(String changeDe) { this.changeDe = changeDe; } + public String getDetailSn() { return detailSn; } + public void setDetailSn(String detailSn) { this.detailSn = detailSn; } + public String getFlag() { return flag; } + public void setFlag(String flag) { this.flag = flag; } + } +} diff --git a/src/main/java/com/vmis/interfaceapp/service/RequestEnricher.java b/src/main/java/com/vmis/interfaceapp/service/RequestEnricher.java new file mode 100644 index 0000000..f2674d1 --- /dev/null +++ b/src/main/java/com/vmis/interfaceapp/service/RequestEnricher.java @@ -0,0 +1,63 @@ +package com.vmis.interfaceapp.service; + +import com.vmis.interfaceapp.config.properties.VmisProperties; +import com.vmis.interfaceapp.model.basic.BasicRequest; +import com.vmis.interfaceapp.model.common.Envelope; +import com.vmis.interfaceapp.model.ledger.LedgerRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/** + * Populates incoming request models with values from YAML configuration. + * Unconditionally overwrites the listed fields per requirement: + * - INFO_SYS_ID, INFO_SYS_IP, SIGUNGU_CODE + * - CNTC_INFO_CODE (service specific) + * - CHARGER_ID, CHARGER_IP, CHARGER_NM + */ +@Component +public class RequestEnricher { + private static final Logger log = LoggerFactory.getLogger(RequestEnricher.class); + + private final VmisProperties props; + + public RequestEnricher(VmisProperties props) { + this.props = props; + } + + public void enrichBasic(Envelope envelope) { + if (envelope == null || envelope.getData() == null) return; + VmisProperties.SystemProps sys = props.getSystem(); + String cntc = props.getGov().getServices().getBasic().getCntcInfoCode(); + for (BasicRequest req : envelope.getData()) { + if (req == null) continue; + req.setInfoSysId(sys.getInfoSysId()); + req.setInfoSysIp(sys.getInfoSysIp()); + req.setSigunguCode(sys.getRegionCode()); + req.setCntcInfoCode(cntc); + req.setChargerId(sys.getChargerId()); + req.setChargerIp(sys.getChargerIp()); + req.setChargerNm(sys.getChargerNm()); + } + log.debug("[ENRICH] basic: applied INFO_SYS_ID={}, INFO_SYS_IP={}, SIGUNGU_CODE={}, CNTC_INFO_CODE={}", + sys.getInfoSysId(), sys.getInfoSysIp(), sys.getRegionCode(), cntc); + } + + public void enrichLedger(Envelope envelope) { + if (envelope == null || envelope.getData() == null) return; + VmisProperties.SystemProps sys = props.getSystem(); + String cntc = props.getGov().getServices().getLedger().getCntcInfoCode(); + for (LedgerRequest req : envelope.getData()) { + if (req == null) continue; + req.setInfoSysId(sys.getInfoSysId()); + req.setInfoSysIp(sys.getInfoSysIp()); + req.setSigunguCode(sys.getRegionCode()); + req.setCntcInfoCode(cntc); + req.setChargerId(sys.getChargerId()); + req.setChargerIp(sys.getChargerIp()); + req.setChargerNm(sys.getChargerNm()); + } + log.debug("[ENRICH] ledger: applied INFO_SYS_ID={}, INFO_SYS_IP={}, SIGUNGU_CODE={}, CNTC_INFO_CODE={}", + sys.getInfoSysId(), sys.getInfoSysIp(), sys.getRegionCode(), cntc); + } +} diff --git a/src/main/java/com/vmis/interfaceapp/util/GpkiCryptoUtil.java b/src/main/java/com/vmis/interfaceapp/util/GpkiCryptoUtil.java new file mode 100644 index 0000000..754b4a3 --- /dev/null +++ b/src/main/java/com/vmis/interfaceapp/util/GpkiCryptoUtil.java @@ -0,0 +1,98 @@ +package com.vmis.interfaceapp.util; + +import com.vmis.interfaceapp.config.properties.VmisProperties; +import lombok.Getter; +import lombok.Setter; + +/** + * Wrapper utility around legacy {@link NewGpkiUtil} using configuration from YAML. + * + * Notes: + * - Place this class under src/main/java/util as requested. + * - Uses Lombok for getters/setters. + */ +@Getter +@Setter +public class GpkiCryptoUtil { + + private String gpkiLicPath; + private Boolean ldap; // null -> legacy default + private String certFilePath; + private String envCertFilePathName; + private String envPrivateKeyFilePathName; + private String envPrivateKeyPasswd; + private String sigCertFilePathName; + private String sigPrivateKeyFilePathName; + private String sigPrivateKeyPasswd; + private String myServerId; // equals to certServerId (INFO system server cert id) + private String targetServerIdList; // comma joined list (can be single id) + + private transient NewGpkiUtil delegate; + + public static GpkiCryptoUtil from(VmisProperties.GpkiProps props) throws Exception { + GpkiCryptoUtil util = new GpkiCryptoUtil(); + util.setGpkiLicPath(props.getGpkiLicPath()); + util.setLdap(props.getLdap()); + util.setCertFilePath(props.getCertFilePath()); + util.setEnvCertFilePathName(props.getEnvCertFilePathName()); + util.setEnvPrivateKeyFilePathName(props.getEnvPrivateKeyFilePathName()); + util.setEnvPrivateKeyPasswd(props.getEnvPrivateKeyPasswd()); + util.setSigCertFilePathName(props.getSigCertFilePathName()); + util.setSigPrivateKeyFilePathName(props.getSigPrivateKeyFilePathName()); + util.setSigPrivateKeyPasswd(props.getSigPrivateKeyPasswd()); + util.setMyServerId(props.getCertServerId()); + // Accept single targetServerId but allow list if provided by YAML in future + util.setTargetServerIdList(props.getTargetServerId()); + util.initialize(); + return util; + } + + public void initialize() throws Exception { + NewGpkiUtil g = new NewGpkiUtil(); + if (gpkiLicPath != null) g.setGpkiLicPath(gpkiLicPath); + if (ldap != null) g.setIsLDAP(ldap); + if (certFilePath != null) g.setCertFilePath(certFilePath); + if (envCertFilePathName != null) g.setEnvCertFilePathName(envCertFilePathName); + if (envPrivateKeyFilePathName != null) g.setEnvPrivateKeyFilePathName(envPrivateKeyFilePathName); + if (envPrivateKeyPasswd != null) g.setEnvPrivateKeyPasswd(envPrivateKeyPasswd); + if (sigCertFilePathName != null) g.setSigCertFilePathName(sigCertFilePathName); + if (sigPrivateKeyFilePathName != null) g.setSigPrivateKeyFilePathName(sigPrivateKeyFilePathName); + if (sigPrivateKeyPasswd != null) g.setSigPrivateKeyPasswd(sigPrivateKeyPasswd); + if (myServerId != null) g.setMyServerId(myServerId); + if (targetServerIdList != null) g.setTargetServerIdList(targetServerIdList); + g.init(); + this.delegate = g; + } + + public String encryptToBase64(String plain, String targetServerId, String charset) throws Exception { + ensureInit(); + byte[] enc = delegate.encrypt(plain.getBytes(charset), targetServerId, true); + return delegate.encode(enc); + } + + public String decryptFromBase64(String base64, String charset) throws Exception { + ensureInit(); + byte[] bin = delegate.decode(base64); + byte[] dec = delegate.decrypt(bin); + return new String(dec, charset); + } + + public String signToBase64(String plain, String charset) throws Exception { + ensureInit(); + byte[] sig = delegate.sign(plain.getBytes(charset)); + return delegate.encode(sig); + } + + public String verifyAndExtractBase64(String signedBase64, String charset) throws Exception { + ensureInit(); + byte[] signed = delegate.decode(signedBase64); + byte[] data = delegate.validate(signed); + return new String(data, charset); + } + + private void ensureInit() { + if (delegate == null) { + throw new IllegalStateException("GpkiCryptoUtil is not initialized. Call initialize() or from(props)."); + } + } +} diff --git a/src/main/java/com/vmis/interfaceapp/util/NewGpkiUtil.java b/src/main/java/com/vmis/interfaceapp/util/NewGpkiUtil.java new file mode 100644 index 0000000..6dc2b00 --- /dev/null +++ b/src/main/java/com/vmis/interfaceapp/util/NewGpkiUtil.java @@ -0,0 +1,383 @@ +package com.vmis.interfaceapp.util; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import org.apache.log4j.Logger; + +import com.gpki.gpkiapi_jni; +import com.gpki.gpkiapi.GpkiApi; +import com.gpki.gpkiapi.cert.X509Certificate; +import com.gpki.gpkiapi.crypto.PrivateKey; +import com.gpki.gpkiapi.storage.Disk; + +public class NewGpkiUtil { + private static Logger logger = Logger.getLogger(NewGpkiUtil.class); + byte[] myEnvCert, myEnvKey, mySigCert, mySigKey; + private Map targetServerCertMap = new HashMap(); + + // properties + private String myServerId; + private String targetServerIdList; + private String envCertFilePathName; + private String envPrivateKeyFilePathName; + private String envPrivateKeyPasswd; + private String sigCertFilePathName; + private String sigPrivateKeyFilePathName; + private String sigPrivateKeyPasswd; + private String certFilePath; + private String gpkiLicPath = "."; + private boolean isLDAP; + private boolean testGPKI = false; + + + public void init() throws Exception { + GpkiApi.init(gpkiLicPath); + gpkiapi_jni gpki = this.getGPKI(); + if(logger.isDebugEnabled()){ + if(gpki.API_GetInfo()==0) + logger.debug(gpki.sReturnString); + else + logger.error(gpki.sDetailErrorString); + } + if(targetServerIdList!=null){ + String certIdList[] = targetServerIdList.split(","); + for(int i = 0 ; i < certIdList.length ; i++){ + String certId = certIdList[i].trim(); + if(!certId.equals("")){ + load(gpki, certId); + } + } + } + + logger.info("Loading gpki certificate : myServerId=" + + this.getMyServerId()); + + X509Certificate _myEnvCert = Disk.readCert(this + .getEnvCertFilePathName()); + myEnvCert = _myEnvCert.getCert(); + + PrivateKey _myEnvKey = Disk.readPriKey(this + .getEnvPrivateKeyFilePathName(), this.getEnvPrivateKeyPasswd()); + myEnvKey = _myEnvKey.getKey(); + + X509Certificate _mySigCert = Disk.readCert(this + .getSigCertFilePathName()); + mySigCert = _mySigCert.getCert(); + + PrivateKey _mySigKey = Disk.readPriKey(this + .getSigPrivateKeyFilePathName(), this.getSigPrivateKeyPasswd()); + mySigKey = _mySigKey.getKey(); + + //test my cert GPKI + if(testGPKI){ + load(gpki, this.getMyServerId()); + testGpki(gpki); + } + this.finish(gpki); + logger.info("GpkiUtil initialized"); + } + + private void load(gpkiapi_jni gpki, String certId) throws Exception { + + logger.debug("Loading gpki certificate : targetServerId="+ certId); + + X509Certificate cert = targetServerCertMap.get(certId); + if (cert != null) { + return; + } + + if (isLDAP) { +// String ldapUrl = "ldap://10.1.7.140:389/cn="; +// String ldapUrl = "ldap://ldap.gcc.go.kr:389/cn="; + String ldapUrl = "ldap://10.1.7.118:389/cn="; // 행정망인 경우 +// String ldapUrl = "ldap://152.99.57.127:389/cn="; // 인터넷망인 경우 + String ldapUri; + if (certId.charAt(3) > '9') { + ldapUri = ",ou=Group of Server,o=Public of Korea,c=KR"; + } else { + ldapUri = ",ou=Group of Server,o=Government of Korea,c=KR"; + } + + int ret = gpki.LDAP_GetAnyDataByURL("userCertificate;binary", ldapUrl + certId + ldapUri); + this.checkResult(ret, gpki); + cert = new X509Certificate(gpki.baReturnArray); + } else { + if(certFilePath != null){ + cert = Disk.readCert(certFilePath + File.separator + certId + ".cer"); + }else{ + logger.debug("not certFilePath"); + } + } + + targetServerCertMap.put(certId, cert); + } + + private gpkiapi_jni getGPKI(){ + gpkiapi_jni gpki = new gpkiapi_jni(); + if(gpki.API_Init(gpkiLicPath) != 0){ + logger.error(gpki.sDetailErrorString); + } + return gpki; + } + private void finish(gpkiapi_jni gpki){ + if(gpki.API_Finish() != 0){ + logger.error(gpki.sDetailErrorString); + } + } + + public byte[] encrypt(byte[] plain, String certId , boolean load) throws Exception { + X509Certificate targetEnvCert = targetServerCertMap.get(certId); + if (targetEnvCert == null) { + throw new Exception("Certificate not found : targetServerId=" + certId); + } + + gpkiapi_jni gpki = this.getGPKI(); + try{ + int result = gpki.CMS_MakeEnvelopedData(targetEnvCert.getCert(), plain, + gpkiapi_jni.SYM_ALG_NEAT_CBC); + checkResult(result, "Fail to encrypt message", gpki); + + return gpki.baReturnArray; + }catch(Exception ex){ + throw ex; + }finally{ + finish(gpki); + } + } + + public byte[] encrypt(byte[] plain, String certId) throws Exception { + return encrypt(plain,certId , false); + } + + public byte[] decrypt(byte[] encrypted) throws Exception { + + gpkiapi_jni gpki = this.getGPKI(); + try{ + int result = gpki.CMS_ProcessEnvelopedData(myEnvCert, myEnvKey, + encrypted); + checkResult(result, "Fail to decrpyt message", gpki); + + return gpki.baReturnArray; + }catch(Exception ex){ + throw ex; + }finally{ + finish(gpki); + } + } + + public byte[] sign(byte[] plain) throws Exception { + + gpkiapi_jni gpki = this.getGPKI(); + try{ + int result = gpki.CMS_MakeSignedData(mySigCert, mySigKey, plain, null); + checkResult(result, "Fail to sign message", gpki); + + return gpki.baReturnArray; + }catch(Exception ex){ + throw ex; + }finally{ + finish(gpki); + } + } + + public byte[] validate(byte[] signed) throws Exception { + + gpkiapi_jni gpki = this.getGPKI(); + try{ + int result = gpki.CMS_ProcessSignedData(signed); + checkResult(result, "Fail to validate signed message", gpki); + return gpki.baData; + }catch(Exception ex){ + throw ex; + }finally{ + finish(gpki); + } + } + + public String encode(byte[] plain) throws Exception { + + gpkiapi_jni gpki = this.getGPKI(); + try{ + int result = gpki.BASE64_Encode(plain); + checkResult(result, "Fail to encode message", gpki); + + return gpki.sReturnString; + }catch(Exception ex){ + throw ex; + }finally{ + finish(gpki); + } + + } + + public byte[] decode(String base64) throws Exception { + + gpkiapi_jni gpki = this.getGPKI(); + try{ + int result = gpki.BASE64_Decode(base64); + checkResult(result, "Fail to decode base64 message", gpki); + + return gpki.baReturnArray; + }catch(Exception ex){ + throw ex; + }finally{ + finish(gpki); + } + } + + private void checkResult(int result, gpkiapi_jni gpki)throws Exception{ + this.checkResult(result, null, gpki); + } + + private void checkResult(int result ,String message, gpkiapi_jni gpki)throws Exception{ + if( 0 != result){ + if(null != gpki){ + throw new Exception(message + " : gpkiErrorMessage=" + gpki.sDetailErrorString); + }else{ + throw new Exception(message + " : gpkiErrorCode=" + result); + } + } + } + + public void testGpki(gpkiapi_jni gpki) throws Exception{ + //gpki test eng + logger.info("======================================================="); + logger.info("================ TEST GPKI START ======================"); + logger.info("======================================================="); + String original_Eng = "abc"; + logger.info("=== TEST ENG STRING: "+ original_Eng); + try { + byte[] encrypted = encrypt(original_Eng.getBytes(), myServerId); + logger.info("=== TEST ENG ENCRYPT STRING: "+ encode(encrypted)); + String decrypted = new String(decrypt(encrypted)); + logger.info("=== TEST ENG DECRYPT STRING: "+decrypted); + + if (!original_Eng.equals(decrypted)) { + throw new Exception("GpkiUtil not initialized properly(english)"); + } + logger.info("=== TEST ENG: OK"); + } catch (Exception e) { + logger.warn("Gpki Test error(english)", e); + throw e; + } + //gpki test kor + String original = "한글테스트"; + logger.info("=== TEST KOR STRING: "+ original); + try { + byte[] encrypted = encrypt(original.getBytes(), myServerId); + logger.info("=== TEST KOR ENCRYPT STRING: "+ encode(encrypted)); + String decrypted = new String(decrypt(encrypted)); + logger.info("=== TEST KOR DECRYPT STRING: "+decrypted); + if (!original.equals(decrypted)) { + throw new Exception("GpkiUtil not initialized properly(korean)"); + } + logger.info("=== TEST KOR: OK"); + } catch (Exception e) { + logger.warn("Gpki Test error(korean)", e); + throw e; + }finally{ + logger.info("======================================================="); + logger.info("================ TEST GPKI END ========================"); + logger.info("======================================================="); + } + } + + public String getMyServerId() { + return myServerId; + } + + public void setMyServerId(String myServerId) { + this.myServerId = myServerId.trim(); + } + + public String getEnvCertFilePathName() { + return envCertFilePathName; + } + + public void setEnvCertFilePathName(String envCertFilePathName) { + this.envCertFilePathName = envCertFilePathName.trim(); + } + + public String getEnvPrivateKeyFilePathName() { + return envPrivateKeyFilePathName; + } + + public void setEnvPrivateKeyFilePathName(String envPrivateKeyFilePathName) { + this.envPrivateKeyFilePathName = envPrivateKeyFilePathName.trim(); + } + + public String getEnvPrivateKeyPasswd() { + return envPrivateKeyPasswd; + } + + public void setEnvPrivateKeyPasswd(String envPrivateKeyPasswd) { + this.envPrivateKeyPasswd = envPrivateKeyPasswd.trim(); + } + + public String getSigPrivateKeyPasswd() { + return sigPrivateKeyPasswd; + } + + public void setSigPrivateKeyPasswd(String sigPrivateKeyPasswd) { + this.sigPrivateKeyPasswd = sigPrivateKeyPasswd.trim(); + } + + public String getSigCertFilePathName() { + return sigCertFilePathName; + } + + public void setSigCertFilePathName(String sigCertFilePathName) { + this.sigCertFilePathName = sigCertFilePathName.trim(); + } + + public String getSigPrivateKeyFilePathName() { + return sigPrivateKeyFilePathName; + } + + public void setSigPrivateKeyFilePathName(String sigPrivateKeyFilePathName) { + this.sigPrivateKeyFilePathName = sigPrivateKeyFilePathName.trim(); + } + + public boolean getIsLDAP() { + return isLDAP; + } + + public void setIsLDAP(boolean isLDAP) { + this.isLDAP = isLDAP; + } + + public String getCertFilePath() { + return certFilePath; + } + + public void setCertFilePath(String certFilePath) { + this.certFilePath = certFilePath.trim(); + } + + public String getTargetServerIdList() { + return targetServerIdList; + } + + public void setTargetServerIdList(String targetServerIdList) { + this.targetServerIdList = targetServerIdList; + } + + public String getGpkiLicPath() { + return gpkiLicPath; + } + + public void setGpkiLicPath(String gpkiLicPath) { + this.gpkiLicPath = gpkiLicPath; + } + + public boolean getTestGPKI() { + return testGPKI; + } + + public void setTestGPKI(boolean testGPKI) { + this.testGPKI = testGPKI; + } + +} diff --git a/src/main/java/com/vmis/interfaceapp/util/TxIdUtil.java b/src/main/java/com/vmis/interfaceapp/util/TxIdUtil.java new file mode 100644 index 0000000..067bf79 --- /dev/null +++ b/src/main/java/com/vmis/interfaceapp/util/TxIdUtil.java @@ -0,0 +1,18 @@ +package com.vmis.interfaceapp.util; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Random; + +public final class TxIdUtil { + private static final Random RANDOM = new Random(); + + private TxIdUtil() {} + + public static String generate() { + String time = new SimpleDateFormat("yyyyMMddHHmmssSSS", Locale.KOREA).format(new Date()); + int random = 100000 + RANDOM.nextInt(900000); + return time + "_" + random; + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..18c5ae7 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,45 @@ +server: + port: 8080 + +# 인터페이스 및 연계 설정 - 개발(DEV) 환경 +vmis: + system: + infoSysId: "41-345" + infoSysIp: "105.19.10.135" + regionCode: "41460" + departmentCode: "" + chargerId: "" + chargerIp: "" + chargerNm: "" + gpki: + enabled: "Y" + useSign: true + charset: "UTF-8" + certServerId: "SVR5640020001" + targetServerId: "SVR1500000015" + ldap: true + gpkiLicPath: "C:/gpki2/gpkisecureweb/conf" + certFilePath: "C:/gpki2/gpkisecureweb/certs" + envCertFilePathName: "C:/gpki2/gpkisecureweb/certs/SVR5640020001_env.cer" + envPrivateKeyFilePathName: "C:/gpki2/gpkisecureweb/certs/SVR5640020001_env.key" + envPrivateKeyPasswd: "change-me-dev" + sigCertFilePathName: "C:/gpki2/gpkisecureweb/certs/SVR5640020001_sig.cer" + sigPrivateKeyFilePathName: "C:/gpki2/gpkisecureweb/certs/SVR5640020001_sig.key" + sigPrivateKeyPasswd: "change-me-dev" + gov: + scheme: "http" + host: "10.188.225.94:29001" # 개발(DEV) 행정망 + basePath: "/piss/api/molit" + connectTimeoutMillis: 5000 + readTimeoutMillis: 10000 + services: + basic: # 시군구연계 자동차기본사항조회 + path: "/SignguCarBassMatterInqireService" + cntcInfoCode: "AC1_FD11_01" + apiKey: "05e8d748fb366a0831dce71a32424460746a72d591cf483ccc130534dd51e394" + cvmisApikey: "014F9215-B6D9A3B6-4CED5225-68408C46" + ledger: # 시군구연계 자동차등록원부(갑) + path: "/SignguCarLedgerFrmbkService" + cntcInfoCode: "AC1_FD11_02" + apiKey: "1beeb01857c2e7e9b41c002b007ccb9754d9c272f66d4bb64fc45b302c69e529" + cvmisApikey: "63DF159B-7B9C64C5-86CCB15C-5F93E750" diff --git a/src/main/resources/application-prd.yml b/src/main/resources/application-prd.yml new file mode 100644 index 0000000..d771f24 --- /dev/null +++ b/src/main/resources/application-prd.yml @@ -0,0 +1,42 @@ +server: + port: 8080 + +# 인터페이스 및 연계 설정 - 운영(PRD) 환경 +# 주의: 실제 운영 키/호스트는 배포 환경 변수나 외부 설정(Secret)로 주입 권장 +vmis: + system: + infoSysId: "41-345" # 운영 실제값으로 교체 + regionCode: "" # 운영 실제값 + departmentCode: "" # 운영 실제값 + gpki: + enabled: "Y" + useSign: true + charset: "UTF-8" + certServerId: "SVR5640020001" # 운영 인증서 ID로 교체 + targetServerId: "SVR1500000015" + ldap: true + gpkiLicPath: "C:/gpki2/gpkisecureweb/conf" + certFilePath: "C:/gpki2/gpkisecureweb/certs" + envCertFilePathName: "C:/gpki2/gpkisecureweb/certs/SVR5640020001_env.cer" + envPrivateKeyFilePathName: "C:/gpki2/gpkisecureweb/certs/SVR5640020001_env.key" + envPrivateKeyPasswd: "change-me-prd" + sigCertFilePathName: "C:/gpki2/gpkisecureweb/certs/SVR5640020001_sig.cer" + sigPrivateKeyFilePathName: "C:/gpki2/gpkisecureweb/certs/SVR5640020001_sig.key" + sigPrivateKeyPasswd: "change-me-prd" + gov: + scheme: "http" + host: "10.188.225.25:29001" # 예시: 운영 행정망 (명세에 맞춰 수정) + basePath: "/piss/api/molit" + connectTimeoutMillis: 5000 + readTimeoutMillis: 10000 + services: + basic: + path: "/SignguCarBassMatterInqireService" + cntcInfoCode: "AC1_FD11_01" + apiKey: "05e8d748fb366a0831dce71a32424460746a72d591cf483ccc130534dd51e394" + cvmisApikey: "014F9215-B6D9A3B6-4CED5225-68408C46" + ledger: + path: "/SignguCarLedgerFrmbkService" + cntcInfoCode: "AC1_FD11_02" + apiKey: "1beeb01857c2e7e9b41c002b007ccb9754d9c272f66d4bb64fc45b302c69e529" + cvmisApikey: "63DF159B-7B9C64C5-86CCB15C-5F93E750" diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..260e017 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,55 @@ +spring: + application: + name: vmis-interface + mvc: + pathmatch: + matching-strategy: ant_path_matcher + +server: + port: 8080 + +# 인터페이스 및 연계 설정 (공통 기본값) +vmis: + system: + infoSysId: "41-345" # INFO_SYS_ID + infoSysIp: "" # INFO_SYS_IP (예: 105.19.10.135) + # 예시: 경기도 용인시 수지구 장애인복지과 + regionCode: "" # 시군구코드 (SIGUNGU_CODE, 예: 41460) + departmentCode: "" # 행정부서코드 필요 시 입력 + # 담당자 기본값(미입력 시 빈 값) + chargerId: "" + chargerIp: "" + chargerNm: "" + gpki: + enabled: "Y" # Y 또는 N (환경별 yml에서 재정의 가능) + useSign: true + charset: "UTF-8" + certServerId: "SVR5640020001" # 이용기관 인증서 ID (ENV/SIG 공통 서버 ID) + targetServerId: "SVR1611000006" # 보유기관 인증서 ID(교통안전공단) + # 아래는 네이티브 GPKI 유틸(NewGpkiUtil) 설정값 (필요 시 사용) + ldap: true # LDAP 사용 여부 (true: LDAP에서 대상서버 인증서 조회) + gpkiLicPath: "C:/gpki2/gpkisecureweb/conf" + certFilePath: "C:/gpki2/gpkisecureweb/certs" # LDAP 미사용 시 대상서버 인증서 파일 폴더 + envCertFilePathName: "C:/gpki2/gpkisecureweb/certs/SVRxxxx_env.cer" + envPrivateKeyFilePathName: "C:/gpki2/gpkisecureweb/certs/SVRxxxx_env.key" + envPrivateKeyPasswd: "change-me" + sigCertFilePathName: "C:/gpki2/gpkisecureweb/certs/SVRxxxx_sig.cer" + sigPrivateKeyFilePathName: "C:/gpki2/gpkisecureweb/certs/SVRxxxx_sig.key" + sigPrivateKeyPasswd: "change-me" + gov: + scheme: "http" + host: "localhost:18080" # 환경별 yml에서 재정의 + basePath: "/piss/api/molit" + connectTimeoutMillis: 5000 + readTimeoutMillis: 10000 + services: + basic: # 시군구연계 자동차기본사항조회 + path: "/SignguCarBassMatterInqireService" + cntcInfoCode: "AC1_FD11_01" + apiKey: "change-me" + cvmisApikey: "change-me" + ledger: # 시군구연계 자동차등록원부(갑) + path: "/SignguCarLedgerFrmbkService" + cntcInfoCode: "AC1_FD11_02" + apiKey: "change-me" + cvmisApikey: "change-me" diff --git a/참고자료/gpki 암호화 원시소스/N0000001589_차세대교통안전공단_시군구연계_자동차기본사항조회_SignguCarBassMatterInqireService.java b/참고자료/gpki 암호화 원시소스/N0000001589_차세대교통안전공단_시군구연계_자동차기본사항조회_SignguCarBassMatterInqireService.java new file mode 100644 index 0000000..06da35d --- /dev/null +++ b/참고자료/gpki 암호화 원시소스/N0000001589_차세대교통안전공단_시군구연계_자동차기본사항조회_SignguCarBassMatterInqireService.java @@ -0,0 +1,316 @@ + + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.Charset; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Random; + +import util.NewGpkiUtil; +import util.ShareGpki; + +public class N0000001589_차세대교통안전공단_시군구연계_자동차기본사항조회_SignguCarBassMatterInqireService { + + /** + * IF-LI-AC1-FD11-01-GSR0 + * 자동차기본사항 + */ + + // 이용기관의 네트워크망에 따라서 호출 host 정보가 다름. (네트워크망과 개발 or 운영의 용도에 따라서 해당하는 host로 설정) + + // 이용기관 : 행정망 + static String host = "10.188.225.94:29001"; // 개발(DEV) +// static String host = "10.188.225.25:29001"; // 운영(OP) + + // 이용기관 : 인터넷 +// static String host = "116.67.73.153:39011"; // 개발(DEV) +// static String host = "116.67.73.153:29001"; // 운영(OP) + + static String targetUrl = "http://"+host+"/piss/api/molit/SignguCarBassMatterInqireService"; + static String method = "POST"; + + //--------------Client Setting--------------- + //http Header Setting + static String requestType = "json"; + static String apiKey = "행정정보공동이용센터 API KEY"; // 행정정보공동이용센터에서 발급 + static String cvmis_apikey = "교통안전공단 cvmis_apikey"; // 교통안전공단에서 발급 + static boolean useMockResponse = true; // true false + //http Body Setting + static String charset = "UTF-8"; + + //Gpki Setting + static boolean useGpki = true; + static boolean useSign = true; + static String gpkiCharset = "UTF-8"; + static String certServerId = "이용기관 인증서"; // 이용기관 인증서 ID + static String targetServerId = "SVR1611000006"; // 보유기관 인증서 ID(교통안전공단) + public static void main(String[] args) { + + N0000001589_차세대교통안전공단_시군구연계_자동차기본사항조회_SignguCarBassMatterInqireService.executeClient(); + } + public static void executeClient(){ + String requestBody = makeBody(); + String tx_id = getTx_Id(); + + Map requestHeader = new LinkedHashMap(); + try { + requestHeader.put("Host", InetAddress.getLocalHost().toString()); + requestHeader.put("User-Agent", "java-net-httpclient"); + } catch (Exception e) { + e.printStackTrace(); + } + requestHeader.put("Content-Type", "application/"+requestType); + requestHeader.put("Accept", "application/"+requestType); + requestHeader.put("gpki_yn",useGpki ? "Y":"N"); +// requestHeader.put("mock_yn",useMockResponse ? "Y":"N"); + requestHeader.put("tx_id", tx_id); + requestHeader.put("cert_server_id",certServerId); + requestHeader.put("api_key", apiKey); + requestHeader.put("cvmis_apikey", cvmis_apikey); + + System.out.println("-------- Request Header -----------"); + for(Map.Entry entry : requestHeader.entrySet()){ + System.out.println(entry.getKey() + " : " + entry.getValue()); + } + System.out.println("--------- Request Body ------------"); + System.out.println(requestBody); + + // body 암호화 + if(useGpki) { + System.out.println("----- Request Body Encrypt --------"); + String bodyEncryptTarget = requestBody; + String bodyEncrypt = ""; + try { + bodyEncrypt = gpkiEncrypt(bodyEncryptTarget.toString()); + requestBody = requestBody.replace(bodyEncryptTarget, bodyEncrypt); + System.out.println("Encrypt Body : " + requestBody); + } catch (Exception e) { + e.printStackTrace(); + } + } + System.out.println("------------ Response -------------"); + N0000001589_차세대교통안전공단_시군구연계_자동차기본사항조회_SignguCarBassMatterInqireService.Response response = sendRequest(targetUrl, method, requestHeader, requestBody, charset); + + int responseCode = response.getResponseCode(); + Map responseHeader = response.getHeaderMap(); + String responseBody = response.getBody(); + + System.out.println("-> response : " + response); + System.out.println("-> responseCode : " + responseCode); + System.out.println("-> responseHeader : " + responseHeader); + System.out.println("-> responseBody : " + responseBody); + + // Body 복호화 + if(useGpki && responseCode == HttpURLConnection.HTTP_OK) { + System.out.println("----- Response Body Decrypt --------"); + + try { + String bodyDecryptTarget = responseBody; + String bodyDecrypt = ""; + + bodyDecrypt = gpkiDecrypt(bodyDecryptTarget); + responseBody = responseBody.replace(bodyDecryptTarget, bodyDecrypt); + } catch (Exception e) { + e.printStackTrace(); + } + } + System.out.println("-------- Response Message ---------"); + System.out.println(responseBody); + } + private static Response sendRequest(String targetUrl, String method, + Map requestHeader, String body, String charset) { + Response response = new Response(); + + HttpURLConnection con = null; + try { + URL url = new URL(targetUrl); + con = (HttpURLConnection) url.openConnection(); + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + + try { + for(Map.Entry entry : requestHeader.entrySet()){ + con.setRequestProperty(entry.getKey(), entry.getValue()); + } + con.setRequestMethod(method); + con.setDefaultUseCaches(false); + con.setAllowUserInteraction(false); + con.setReadTimeout(15000); + con.setConnectTimeout(3000); + con.setDoInput(true); + con.setDoOutput(true); + + con.connect(); + + OutputStream os = null; + OutputStreamWriter osw = null; + StringBuffer responseBodyBuffer = new StringBuffer(""); + Map headerMap = response.getHeaderMap(); + + os = con.getOutputStream(); + osw = new OutputStreamWriter(os, Charset.forName(charset)); + osw.write(body); + osw.flush(); + + int code = con.getResponseCode(); + response.setResponseCode(code); + + InputStream is = null; + InputStreamReader isr = null; + if(code == HttpURLConnection.HTTP_OK){ + is = con.getInputStream(); + }else{ + is = con.getErrorStream(); + } + + Map> headerFields = con.getHeaderFields(); + for(Map.Entry> headerEntry : headerFields.entrySet()){ + String key = headerEntry.getKey(); + List values = headerEntry.getValue(); + if(values.size()<=1){ + headerMap.put(key, values.get(0)); + }else{ + headerMap.put(key, values); + } + } + try { + isr = new InputStreamReader(is, Charset.forName(charset)); + int len = -1; + char[] ch = new char[32]; + while((len = isr.read(ch, 0, ch.length))!= -1){ + responseBodyBuffer.append(new String(ch, 0, len)); + } + response.setBody(responseBodyBuffer.toString()); + } catch (IOException e) { + e.printStackTrace(); + }finally{ + isr.close(); + is.close(); + os.close(); + osw.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } finally{ + if(con != null){ + con.disconnect(); + } + } + return response; + } + private static String makeBody() { + StringBuffer sb = new StringBuffer(); + // request data + sb.append("{\n"); + sb.append(" \"data\": [\n"); + sb.append(" {\n"); + sb.append(" \"CNTC_INFO_CODE\": \"\",\n"); + sb.append(" \"ONES_INFORMATION_OPEN\": \"\",\n"); + sb.append(" \"VHRNO\": \"\",\n"); + sb.append(" \"CPTTR_NM\": \"\",\n"); + sb.append(" \"CPTTR_IHIDNUM\": \"\",\n"); + sb.append(" \"CPTTR_LEGALDONG_CODE\": \"\",\n"); + sb.append(" \"ROUTE_SE_CODE\": \"\",\n"); + sb.append(" \"DETAIL_EXPRESSION\": \"\",\n"); + sb.append(" \"INQIRE_SE_CODE\": \"\"\n"); + sb.append(" }\n"); + sb.append(" ]\n"); + sb.append("}\n"); + return sb.toString(); + } + + // 암호화 + private static String gpkiEncrypt(String str) throws Exception { + NewGpkiUtil g = ShareGpki.getGpkiUtil(targetServerId); + + byte[] encrypted = g.encrypt(str.getBytes(gpkiCharset), targetServerId); // 암호화 + byte[] signed = encrypted; + if(useSign) { + signed = g.sign(encrypted); // digital sign + } + String encoded = g.encode(signed); // base64 encode + return encoded; + } + + // 복호화 + public static String gpkiDecrypt(String str) throws Exception { + NewGpkiUtil g = ShareGpki.getGpkiUtil(targetServerId); + byte[] decode = g.decode(str); // base64 decode + byte[] validate = decode; + if(useSign) { + validate = g.validate(decode); + } + byte[] decrypt = g.decrypt(validate); // 복호화 + String decrypted = new String(decrypt,gpkiCharset); +// System.out.println("body(encrypted) : " + str); // Testing log +// System.out.println("body(decrypt) : " + decrypted); // Testing log + return decrypted; + } + private static String getTx_Id() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS",Locale.KOREA); + String cur = sdf.format(new Date()); + String transactionUniqueId = cur + keyGen(8); + return transactionUniqueId; + } + private static Random r = new Random(System.currentTimeMillis()); + public static String keyGen(int length) { + char[] key = new char[length]; + int tmp = 0; + for (int i = 0; i < length; i++) { + tmp = r.nextInt(3); + if (tmp == 0) + key[i] = (char) (r.nextInt(26) + 65); + else if (tmp == 1) + key[i] = (char) (r.nextInt(10) + 48); + else if (tmp == 2) + key[i] = (char) (r.nextInt(26) + 97); + else { + key[i] = (char) r.nextInt(256); + } + } + return String.valueOf(key); + } + + /* + * Response + */ + public static class Response { + int responseCode; + String body; + Map headerMap = new LinkedHashMap(); + + public int getResponseCode() { + return responseCode; + } + public void setResponseCode(int responseCode) { + this.responseCode = responseCode; + } + public String getBody() { + return body; + } + public void setBody(String body) { + this.body = body; + } + public Map getHeaderMap() { + return headerMap; + } + public void setHeaderMap(Map headerMap) { + this.headerMap = headerMap; + } + } +} diff --git a/참고자료/gpki 암호화 원시소스/N0000002006_차세대교통안전공단_시군구연계_자동차등록원부갑_SignguCarLedgerFrmbkService.java b/참고자료/gpki 암호화 원시소스/N0000002006_차세대교통안전공단_시군구연계_자동차등록원부갑_SignguCarLedgerFrmbkService.java new file mode 100644 index 0000000..d4a66af --- /dev/null +++ b/참고자료/gpki 암호화 원시소스/N0000002006_차세대교통안전공단_시군구연계_자동차등록원부갑_SignguCarLedgerFrmbkService.java @@ -0,0 +1,311 @@ +package _차세대교통안전공단_시군구_이용기관용; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.Charset; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Random; + +import util.NewGpkiUtil; +import util.ShareGpki; + +public class N0000002006_차세대교통안전공단_시군구연계_자동차등록원부갑_SignguCarLedgerFrmbkService { + + /** + * IF-LI-AC1-FD11-02-GSR0 + * 자동차등록원부(갑) + */ + + // 이용기관의 네트워크망에 따라서 호출 host 정보가 다름. (네트워크망과 개발 or 운영의 용도에 따라서 해당하는 host로 설정) + + // 이용기관 : 행정망 + static String host = "10.188.225.94:29001"; // 개발(DEV) +// static String host = "10.188.225.25:29001"; // 운영(OP) + + // 이용기관 : 인터넷 +// static String host = "116.67.73.153:39011"; // 개발(DEV) +// static String host = "116.67.73.153:29001"; // 운영(OP) + + static String targetUrl = "http://"+host+"/piss/api/molit/SignguCarLedgerFrmbkService"; + static String method = "POST"; + + //--------------Client Setting--------------- + //http Header Setting + static String requestType = "json"; + static String apiKey = "행정정보공동이용센터 API KEY"; // 행정정보공동이용센터에서 발급 + static String cvmis_apikey = "교통안전공단 cvmis_apikey"; // 교통안전공단에서 발급 + static boolean useMockResponse = true; // true false + //http Body Setting + static String charset = "UTF-8"; + + //Gpki Setting + static boolean useGpki = true; + static boolean useSign = true; + static String gpkiCharset = "UTF-8"; + static String certServerId = "이용기관 인증서"; // 이용기관 인증서 ID + static String targetServerId = "SVR1611000006"; // 보유기관 인증서 ID(교통안전공단) + public static void main(String[] args) { + + N0000002006_차세대교통안전공단_시군구연계_자동차등록원부갑_SignguCarLedgerFrmbkService.executeClient(); + } + public static void executeClient(){ + String requestBody = makeBody(); + String tx_id = getTx_Id(); + + Map requestHeader = new LinkedHashMap(); + try { + requestHeader.put("Host", InetAddress.getLocalHost().toString()); + requestHeader.put("User-Agent", "java-net-httpclient"); + } catch (Exception e) { + e.printStackTrace(); + } + requestHeader.put("Content-Type", "application/"+requestType); + requestHeader.put("Accept", "application/"+requestType); + requestHeader.put("gpki_yn",useGpki ? "Y":"N"); +// requestHeader.put("mock_yn",useMockResponse ? "Y":"N"); + requestHeader.put("tx_id", tx_id); + requestHeader.put("cert_server_id",certServerId); + requestHeader.put("api_key", apiKey); + requestHeader.put("cvmis_apikey", cvmis_apikey); + + System.out.println("-------- Request Header -----------"); + for(Map.Entry entry : requestHeader.entrySet()){ + System.out.println(entry.getKey() + " : " + entry.getValue()); + } + System.out.println("--------- Request Body ------------"); + System.out.println(requestBody); + + // body 암호화 + if(useGpki) { + System.out.println("----- Request Body Encrypt --------"); + String bodyEncryptTarget = requestBody; + String bodyEncrypt = ""; + try { + bodyEncrypt = gpkiEncrypt(bodyEncryptTarget.toString()); + requestBody = requestBody.replace(bodyEncryptTarget, bodyEncrypt); + System.out.println("Encrypt Body : " + requestBody); + } catch (Exception e) { + e.printStackTrace(); + } + } + System.out.println("------------ Response -------------"); + N0000002006_차세대교통안전공단_시군구연계_자동차등록원부갑_SignguCarLedgerFrmbkService.Response response = sendRequest(targetUrl, method, requestHeader, requestBody, charset); + + int responseCode = response.getResponseCode(); + Map responseHeader = response.getHeaderMap(); + String responseBody = response.getBody(); + + System.out.println("-> response : " + response); + System.out.println("-> responseCode : " + responseCode); + System.out.println("-> responseHeader : " + responseHeader); + System.out.println("-> responseBody : " + responseBody); + + // Body 복호화 + if(useGpki && responseCode == HttpURLConnection.HTTP_OK) { + System.out.println("----- Response Body Decrypt --------"); + + try { + String bodyDecryptTarget = responseBody; + String bodyDecrypt = ""; + + bodyDecrypt = gpkiDecrypt(bodyDecryptTarget); + responseBody = responseBody.replace(bodyDecryptTarget, bodyDecrypt); + } catch (Exception e) { + e.printStackTrace(); + } + } + System.out.println("-------- Response Message ---------"); + System.out.println(responseBody); + } + private static Response sendRequest(String targetUrl, String method, + Map requestHeader, String body, String charset) { + Response response = new Response(); + + HttpURLConnection con = null; + try { + URL url = new URL(targetUrl); + con = (HttpURLConnection) url.openConnection(); + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + + try { + for(Map.Entry entry : requestHeader.entrySet()){ + con.setRequestProperty(entry.getKey(), entry.getValue()); + } + con.setRequestMethod(method); + con.setDefaultUseCaches(false); + con.setAllowUserInteraction(false); + con.setReadTimeout(15000); + con.setConnectTimeout(3000); + con.setDoInput(true); + con.setDoOutput(true); + + con.connect(); + + OutputStream os = null; + OutputStreamWriter osw = null; + StringBuffer responseBodyBuffer = new StringBuffer(""); + Map headerMap = response.getHeaderMap(); + + os = con.getOutputStream(); + osw = new OutputStreamWriter(os, Charset.forName(charset)); + osw.write(body); + osw.flush(); + + int code = con.getResponseCode(); + response.setResponseCode(code); + + InputStream is = null; + InputStreamReader isr = null; + if(code == HttpURLConnection.HTTP_OK){ + is = con.getInputStream(); + }else{ + is = con.getErrorStream(); + } + + Map> headerFields = con.getHeaderFields(); + for(Map.Entry> headerEntry : headerFields.entrySet()){ + String key = headerEntry.getKey(); + List values = headerEntry.getValue(); + if(values.size()<=1){ + headerMap.put(key, values.get(0)); + }else{ + headerMap.put(key, values); + } + } + try { + isr = new InputStreamReader(is, Charset.forName(charset)); + int len = -1; + char[] ch = new char[32]; + while((len = isr.read(ch, 0, ch.length))!= -1){ + responseBodyBuffer.append(new String(ch, 0, len)); + } + response.setBody(responseBodyBuffer.toString()); + } catch (IOException e) { + e.printStackTrace(); + }finally{ + isr.close(); + is.close(); + os.close(); + osw.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } finally{ + if(con != null){ + con.disconnect(); + } + } + return response; + } + private static String makeBody() { + StringBuffer sb = new StringBuffer(); + // request data + sb.append("{\n"); + sb.append(" \"data\": [\n"); + sb.append(" {\n"); + sb.append(" \"CHARGER_ID\": \"\",\n"); + sb.append(" \"CHARGER_IP\": \"\",\n"); + sb.append(" \"CHARGER_NM\": \"\",\n"); + sb.append(" \"REQUST_VHRNO\": \"\"\n"); + sb.append(" }\n"); + sb.append(" ]\n"); + sb.append("}\n"); + return sb.toString(); + } + + // 암호화 + private static String gpkiEncrypt(String str) throws Exception { + NewGpkiUtil g = ShareGpki.getGpkiUtil(targetServerId); + + byte[] encrypted = g.encrypt(str.getBytes(gpkiCharset), targetServerId); // 암호화 + byte[] signed = encrypted; + if(useSign) { + signed = g.sign(encrypted); // digital sign + } + String encoded = g.encode(signed); // base64 encode + return encoded; + } + + // 복호화 + public static String gpkiDecrypt(String str) throws Exception { + NewGpkiUtil g = ShareGpki.getGpkiUtil(targetServerId); + byte[] decode = g.decode(str); // base64 decode + byte[] validate = decode; + if(useSign) { + validate = g.validate(decode); + } + byte[] decrypt = g.decrypt(validate); // 복호화 + String decrypted = new String(decrypt,gpkiCharset); +// System.out.println("body(encrypted) : " + str); // Testing log +// System.out.println("body(decrypt) : " + decrypted); // Testing log + return decrypted; + } + private static String getTx_Id() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS",Locale.KOREA); + String cur = sdf.format(new Date()); + String transactionUniqueId = cur + keyGen(8); + return transactionUniqueId; + } + private static Random r = new Random(System.currentTimeMillis()); + public static String keyGen(int length) { + char[] key = new char[length]; + int tmp = 0; + for (int i = 0; i < length; i++) { + tmp = r.nextInt(3); + if (tmp == 0) + key[i] = (char) (r.nextInt(26) + 65); + else if (tmp == 1) + key[i] = (char) (r.nextInt(10) + 48); + else if (tmp == 2) + key[i] = (char) (r.nextInt(26) + 97); + else { + key[i] = (char) r.nextInt(256); + } + } + return String.valueOf(key); + } + + /* + * Response + */ + public static class Response { + int responseCode; + String body; + Map headerMap = new LinkedHashMap(); + + public int getResponseCode() { + return responseCode; + } + public void setResponseCode(int responseCode) { + this.responseCode = responseCode; + } + public String getBody() { + return body; + } + public void setBody(String body) { + this.body = body; + } + public Map getHeaderMap() { + return headerMap; + } + public void setHeaderMap(Map headerMap) { + this.headerMap = headerMap; + } + } +} diff --git a/참고자료/인터페이스 정의서/0000001589.별첨.시군구연계 자동차기본사항조회 실시간 서비스 메시지 규격.hwp b/참고자료/인터페이스 정의서/0000001589.별첨.시군구연계 자동차기본사항조회 실시간 서비스 메시지 규격.hwp new file mode 100644 index 0000000..56abf9d Binary files /dev/null and b/참고자료/인터페이스 정의서/0000001589.별첨.시군구연계 자동차기본사항조회 실시간 서비스 메시지 규격.hwp differ diff --git a/참고자료/인터페이스 정의서/0000001589.별첨.시군구연계 자동차기본사항조회 실시간 서비스 메시지 규격.pdf b/참고자료/인터페이스 정의서/0000001589.별첨.시군구연계 자동차기본사항조회 실시간 서비스 메시지 규격.pdf new file mode 100644 index 0000000..e6da2f6 Binary files /dev/null and b/참고자료/인터페이스 정의서/0000001589.별첨.시군구연계 자동차기본사항조회 실시간 서비스 메시지 규격.pdf differ diff --git a/참고자료/인터페이스 정의서/0000002006.별첨.시군구연계 자동차등록원부(갑) 실시간 서비스 메시지 규격.hwp b/참고자료/인터페이스 정의서/0000002006.별첨.시군구연계 자동차등록원부(갑) 실시간 서비스 메시지 규격.hwp new file mode 100644 index 0000000..aed5da7 Binary files /dev/null and b/참고자료/인터페이스 정의서/0000002006.별첨.시군구연계 자동차등록원부(갑) 실시간 서비스 메시지 규격.hwp differ diff --git a/참고자료/인터페이스 정의서/0000002006.별첨.시군구연계 자동차등록원부(갑) 실시간 서비스 메시지 규격.pdf b/참고자료/인터페이스 정의서/0000002006.별첨.시군구연계 자동차등록원부(갑) 실시간 서비스 메시지 규격.pdf new file mode 100644 index 0000000..88ce064 Binary files /dev/null and b/참고자료/인터페이스 정의서/0000002006.별첨.시군구연계 자동차등록원부(갑) 실시간 서비스 메시지 규격.pdf differ diff --git a/참고자료/참고내용.txt b/참고자료/참고내용.txt new file mode 100644 index 0000000..3d75525 --- /dev/null +++ b/참고자료/참고내용.txt @@ -0,0 +1,30 @@ + ● [이용기관 방화벽 오픈] + - 이용기관에서 방화벽 포트오픈 공문요청 수행 + - 방화벽 오픈 가이드(수신처 포함).hwp + + ● [개발가이드] + - 미래행공 실시간 연계 가이드_이용기관.hwp + - 서비스 별 메세지규격서 (예시: 0000001589.별첨.시군구연계 자동차기본사항조회 실시간 서비스 메시지 규격.hwp) + + ● [API-KEY, 호출 URL 정보] + - 교통안전공단 인터페이스별 행정정보공동이용센터 호출 URL 및 API-KEY정보.hwp + + ● [교통안전공단 cvmis key] + - 교통안전공단 차세대 사업단으로 문의 + + ● 이용기관 클라이언트(이용기관용Client_시군구.zip) 기관 별 수정사항 + (1) apiKey : 행정정보공동이용센터 API KEY 입력 + (2) cvmis_apikey : 교통안전공단 cvmis_apikey 입력 (교통안전공단 차세대사업단으로 문의) + (3) certServerId : 이용기관 GPKI 인증서 입력 + (4) src/util/ShareGpki.java : 서버에 있는 라이센스(.lic)와 GPKI 인증서 경로 입력 + (ShareGpki에 적혀있는 경로는 샘플을 적은것이므로 기관 사정에 맞추어 수정하시면 됩니다) +++ 이용기관이 인터넷망인경우 5,6번 추가작업 + (5) host : 116.67.73.153 으로 수정 (주석처리 해놓았으므로 주석변경 하면 됩니다) + (6) com.vmis.interfaceapp.util.NewGpkiUtil.java : ldapUrl 152.99.57.127:389로 수정 (주석처리 해놓았으므로 주석변경 하면 됩니다) + + + +문의 있으시면 아래 번호로 연락 부탁드립니다. + + +감사합니다. \ No newline at end of file