Files
invyone/backend-spring/src/main/java/com/erp/service/ExternalRestApiConnectionService.java
T

554 lines
24 KiB
Java

package com.erp.service;
import com.erp.common.BaseService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
import javax.crypto.Cipher;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import java.util.*;
@Service
@Slf4j
public class ExternalRestApiConnectionService extends BaseService {
@Autowired private CommonService commonService;
@Autowired private ObjectMapper objectMapper;
@Value("${erp.encryption.secret:default-secret-key-change-in-production}")
private String encryptionKey;
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_TAG_LENGTH = 128;
private static final int IV_LENGTH = 16;
// ── 목록 조회 ──────────────────────────────────────────────────────────
public List<Map<String, Object>> getExternalRestApiConnectionList(Map<String, Object> params) {
String rawSearch = (String) params.get("search");
if (rawSearch != null && !rawSearch.isBlank()) {
params.put("search", "%" + rawSearch.trim() + "%");
}
String filterCompany = (String) params.get("filter_company_code");
if (filterCompany != null) {
params.put("filter_company_code", filterCompany);
}
String isActive = (String) params.get("is_active");
if (isActive != null) params.put("is_active", isActive);
String authType = (String) params.get("auth_type");
if (authType != null) params.put("auth_type", authType);
commonService.applyCompanyCodeFilter(params);
List<Map<String, Object>> connections = sqlSession.selectList("externalRestApiConnection.getExternalRestApiConnectionList", params);
for (Map<String, Object> conn : connections) {
parseJsonFields(conn);
decryptAuthConfig(conn);
}
return connections;
}
// ── 상세 조회 ──────────────────────────────────────────────────────────
public Map<String, Object> getExternalRestApiConnectionInfo(Map<String, Object> params) {
commonService.applyCompanyCodeFilter(params);
Map<String, Object> conn = sqlSession.selectOne("externalRestApiConnection.getExternalRestApiConnectionInfo", params);
if (conn != null) {
parseJsonFields(conn);
decryptAuthConfig(conn);
}
return conn;
}
// ── 생성 ──────────────────────────────────────────────────────────────
@Transactional
public Map<String, Object> insertExternalRestApiConnection(Map<String, Object> params) {
validateConnectionData(params);
serializeJsonFields(params);
encryptAuthConfig(params);
if (params.get("timeout") == null) params.put("timeout", 30000);
if (params.get("retry_count") == null) params.put("retry_count", 0);
if (params.get("retry_delay") == null) params.put("retry_delay", 1000);
if (params.get("is_active") == null) params.put("is_active", "Y");
if (params.get("default_method") == null) params.put("default_method", "GET");
if (params.get("save_to_history") == null) params.put("save_to_history", "N");
sqlSession.insert("externalRestApiConnection.insertExternalRestApiConnection", params);
log.info("REST API 연결 생성 성공: {}", params.get("connection_name"));
return params;
}
// ── 수정 ──────────────────────────────────────────────────────────────
@Transactional
public Map<String, Object> updateExternalRestApiConnection(int id, Map<String, Object> params) {
params.put("id", id);
commonService.applyCompanyCodeFilter(params);
Map<String, Object> existing = sqlSession.selectOne("externalRestApiConnection.getExternalRestApiConnectionInfo", params);
if (existing == null) {
return null;
}
serializeJsonFields(params);
encryptAuthConfig(params);
if (params.containsKey("default_body")) {
params.put("has_default_body", "Y");
}
sqlSession.update("externalRestApiConnection.updateExternalRestApiConnection", params);
log.info("REST API 연결 수정 성공: ID {}", id);
return sqlSession.selectOne("externalRestApiConnection.getExternalRestApiConnectionInfo", Map.of("id", id));
}
// ── 삭제 ──────────────────────────────────────────────────────────────
@Transactional
public boolean deleteExternalRestApiConnection(int id, Map<String, Object> params) {
params.put("id", id);
commonService.applyCompanyCodeFilter(params);
int deleted = sqlSession.delete("externalRestApiConnection.deleteExternalRestApiConnection", params);
if (deleted > 0) {
log.info("REST API 연결 삭제 성공: ID {}", id);
return true;
}
return false;
}
// ── 연결 테스트 (테스트 데이터 기반) ────────────────────────────────────
public Map<String, Object> testConnection(Map<String, Object> testRequest, String userCompanyCode) {
long startTime = System.currentTimeMillis();
try {
String baseUrl = (String) testRequest.get("base_url");
String endpoint = (String) testRequest.get("endpoint");
String method = (String) testRequest.getOrDefault("method", "GET");
String authType = (String) testRequest.getOrDefault("auth_type", "none");
Object authConfigObj = testRequest.get("auth_config");
Object headersObj = testRequest.get("headers");
Object bodyObj = testRequest.get("body");
int timeout = testRequest.get("timeout") != null
? Integer.parseInt(testRequest.get("timeout").toString()) : 30000;
String url = buildUrl(baseUrl, endpoint);
HttpHeaders httpHeaders = new HttpHeaders();
applyCustomHeaders(httpHeaders, headersObj);
applyAuthHeaders(httpHeaders, authType, authConfigObj, url, userCompanyCode);
if (bodyObj != null) {
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
}
String bodyStr = null;
if (bodyObj != null) {
bodyStr = bodyObj instanceof String ? (String) bodyObj : objectMapper.writeValueAsString(bodyObj);
}
HttpMethod httpMethod = HttpMethod.valueOf(method.toUpperCase());
RequestEntity<String> requestEntity = new RequestEntity<>(
bodyStr, httpHeaders, httpMethod, URI.create(url));
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.exchange(requestEntity, String.class);
long responseTime = System.currentTimeMillis() - startTime;
boolean success = response.getStatusCode().is2xxSuccessful();
Object responseData = null;
try {
responseData = objectMapper.readValue(response.getBody(), Object.class);
} catch (Exception e) {
responseData = response.getBody();
}
Map<String, Object> result = new LinkedHashMap<>();
result.put("success", success);
result.put("message", success ? "연결 성공" : "연결 실패 (" + response.getStatusCode() + ")");
result.put("response_time", responseTime);
result.put("status_code", response.getStatusCode().value());
result.put("response_data", responseData);
return result;
} catch (Exception e) {
long responseTime = System.currentTimeMillis() - startTime;
log.error("REST API 연결 테스트 오류:", e);
Map<String, Object> result = new LinkedHashMap<>();
result.put("success", false);
result.put("message", "연결 실패");
result.put("response_time", responseTime);
result.put("error_details", e.getMessage());
return result;
}
}
// ── 연결 테스트 (ID 기반) ──────────────────────────────────────────────
public Map<String, Object> testConnectionById(int id, String endpoint) {
Map<String, Object> conn = sqlSession.selectOne("externalRestApiConnection.getExternalRestApiConnectionInfo", Map.of("id", id));
if (conn == null) {
return Map.of("success", false, "message", "연결을 찾을 수 없습니다.");
}
parseJsonFields(conn);
decryptAuthConfig(conn);
String effectiveEndpoint = endpoint != null ? endpoint
: (String) conn.get("endpoint_path");
Map<String, Object> testRequest = new LinkedHashMap<>();
testRequest.put("base_url", conn.get("base_url"));
testRequest.put("endpoint", effectiveEndpoint);
testRequest.put("method", conn.getOrDefault("default_method", "GET"));
testRequest.put("headers", conn.get("default_headers"));
testRequest.put("body", conn.get("default_body"));
testRequest.put("auth_type", conn.get("auth_type"));
testRequest.put("auth_config", conn.get("auth_config"));
testRequest.put("timeout", conn.get("timeout"));
String companyCode = (String) conn.get("company_code");
Map<String, Object> result = testConnection(testRequest, companyCode);
try {
Map<String, Object> updateParams = new HashMap<>();
updateParams.put("id", id);
updateParams.put("last_test_result", Boolean.TRUE.equals(result.get("success")) ? "Y" : "N");
updateParams.put("last_test_message", result.get("message"));
sqlSession.update("externalRestApiConnection.updateExternalRestApiConnectionTestResult", updateParams);
} catch (Exception e) {
log.warn("테스트 결과 저장 실패: {}", e.getMessage());
}
return result;
}
// ── 데이터 조회 (프록시) ────────────────────────────────────────────────
public Map<String, Object> fetchData(int connectionId, String endpoint,
String jsonPath, Map<String, Object> params) {
commonService.applyCompanyCodeFilter(params);
params.put("id", connectionId);
Map<String, Object> conn = sqlSession.selectOne("externalRestApiConnection.getExternalRestApiConnectionInfo", params);
if (conn == null) {
return Map.of("success", false, "message", "REST API 연결을 찾을 수 없습니다.");
}
parseJsonFields(conn);
decryptAuthConfig(conn);
if (!"Y".equals(conn.get("is_active"))) {
return Map.of("success", false, "message", "비활성화된 REST API 연결입니다.");
}
String effectiveEndpoint = endpoint != null ? endpoint
: (String) conn.getOrDefault("endpoint_path", "");
Map<String, Object> testRequest = new LinkedHashMap<>();
testRequest.put("base_url", conn.get("base_url"));
testRequest.put("endpoint", effectiveEndpoint);
testRequest.put("method", conn.getOrDefault("default_method", "GET"));
testRequest.put("headers", conn.get("default_headers"));
testRequest.put("body", conn.get("default_body"));
testRequest.put("auth_type", conn.get("auth_type"));
testRequest.put("auth_config", conn.get("auth_config"));
testRequest.put("timeout", conn.get("timeout"));
String companyCode = (String) conn.get("company_code");
Map<String, Object> testResult = testConnection(testRequest, companyCode);
if (!Boolean.TRUE.equals(testResult.get("success"))) {
return Map.of("success", false,
"message", testResult.getOrDefault("message", "REST API 호출에 실패했습니다."));
}
Object responseData = testResult.get("response_data");
Object extractedData = extractByJsonPath(responseData, jsonPath);
List<Object> dataArray;
if (extractedData == null) {
dataArray = responseData instanceof List ? (List<Object>) responseData
: (responseData != null ? List.of(responseData) : List.of());
} else {
dataArray = extractedData instanceof List ? (List<Object>) extractedData
: List.of(extractedData);
}
List<Map<String, Object>> columns = new ArrayList<>();
if (!dataArray.isEmpty() && dataArray.get(0) instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> firstItem = (Map<String, Object>) dataArray.get(0);
for (Map.Entry<String, Object> entry : firstItem.entrySet()) {
Map<String, Object> col = new LinkedHashMap<>();
col.put("column_name", entry.getKey());
col.put("column_label", entry.getKey());
col.put("data_type", entry.getValue() != null ? entry.getValue().getClass().getSimpleName() : "String");
columns.add(col);
}
}
Map<String, Object> connectionInfo = new LinkedHashMap<>();
connectionInfo.put("connection_id", conn.get("id"));
connectionInfo.put("connection_name", conn.get("connection_name"));
connectionInfo.put("base_url", conn.get("base_url"));
connectionInfo.put("endpoint", effectiveEndpoint);
Map<String, Object> data = new LinkedHashMap<>();
data.put("rows", dataArray);
data.put("columns", columns);
data.put("total", dataArray.size());
data.put("connection_info", connectionInfo);
Map<String, Object> result = new LinkedHashMap<>();
result.put("success", true);
result.put("data", data);
result.put("message", dataArray.size() + "개의 데이터를 조회했습니다.");
return result;
}
// ── Private helpers ────────────────────────────────────────────────────
private String buildUrl(String baseUrl, String endpoint) {
if (endpoint == null || endpoint.isBlank()) return baseUrl;
if (endpoint.startsWith("/")) return baseUrl + endpoint;
return baseUrl + "/" + endpoint;
}
@SuppressWarnings("unchecked")
private void applyCustomHeaders(HttpHeaders httpHeaders, Object headersObj) {
if (headersObj instanceof Map) {
((Map<String, Object>) headersObj).forEach((k, v) ->
httpHeaders.set(k, v != null ? v.toString() : ""));
}
}
@SuppressWarnings("unchecked")
private void applyAuthHeaders(HttpHeaders httpHeaders, String authType,
Object authConfigObj, String url, String companyCode) {
if (authConfigObj == null || "none".equals(authType)) return;
Map<String, Object> authConfig;
if (authConfigObj instanceof Map) {
authConfig = (Map<String, Object>) authConfigObj;
} else {
return;
}
switch (authType) {
case "bearer" -> {
String token = (String) authConfig.get("token");
if (token != null) httpHeaders.setBearerAuth(token);
}
case "basic" -> {
String username = (String) authConfig.get("username");
String password = (String) authConfig.get("password");
if (username != null && password != null) {
httpHeaders.setBasicAuth(username, password);
}
}
case "api-key" -> {
String keyLocation = (String) authConfig.getOrDefault("key_location", "header");
String keyName = (String) authConfig.get("key_name");
String keyValue = (String) authConfig.get("key_value");
if ("header".equals(keyLocation) && keyName != null && keyValue != null) {
httpHeaders.set(keyName, keyValue);
}
}
default -> { }
}
}
private Object extractByJsonPath(Object data, String jsonPath) {
if (jsonPath == null || jsonPath.isBlank() || data == null) return data;
String[] parts = jsonPath.split("\\.");
Object current = data;
for (String part : parts) {
if (current instanceof Map) {
current = ((Map<?, ?>) current).get(part);
} else {
return null;
}
}
return current;
}
private void validateConnectionData(Map<String, Object> params) {
String name = (String) params.get("connection_name");
String baseUrl = (String) params.get("base_url");
if (name == null || name.isBlank()) throw new IllegalArgumentException("연결명은 필수입니다.");
if (baseUrl == null || baseUrl.isBlank()) throw new IllegalArgumentException("기본 URL은 필수입니다.");
}
@SuppressWarnings("unchecked")
private void parseJsonFields(Map<String, Object> row) {
parseJsonField(row, "default_headers");
parseJsonField(row, "auth_config");
}
private void parseJsonField(Map<String, Object> row, String key) {
Object val = row.get(key);
if (val instanceof String str && !str.isBlank()) {
try {
row.put(key, objectMapper.readValue(str, Object.class));
} catch (JsonProcessingException e) {
log.warn("JSON 파싱 실패 ({}): {}", key, e.getMessage());
}
}
}
@SuppressWarnings("unchecked")
private void serializeJsonFields(Map<String, Object> params) {
Object headers = params.get("default_headers");
if (headers != null && !(headers instanceof String)) {
try {
params.put("default_headers", objectMapper.writeValueAsString(headers));
} catch (JsonProcessingException e) {
params.put("default_headers", "{}");
}
}
if (params.get("default_headers") == null) {
params.put("default_headers", "{}");
}
}
@SuppressWarnings("unchecked")
private void encryptAuthConfig(Map<String, Object> params) {
Object authConfigObj = params.get("auth_config");
if (authConfigObj == null) return;
Map<String, Object> authConfig;
if (authConfigObj instanceof Map) {
authConfig = new LinkedHashMap<>((Map<String, Object>) authConfigObj);
} else if (authConfigObj instanceof String str) {
try {
authConfig = objectMapper.readValue(str, Map.class);
} catch (JsonProcessingException e) {
return;
}
} else {
return;
}
encryptField(authConfig, "key_value");
encryptField(authConfig, "token");
encryptField(authConfig, "password");
encryptField(authConfig, "client_secret");
try {
params.put("auth_config", objectMapper.writeValueAsString(authConfig));
} catch (JsonProcessingException e) {
log.error("authConfig 직렬화 실패", e);
}
}
@SuppressWarnings("unchecked")
private void decryptAuthConfig(Map<String, Object> row) {
Object authConfigObj = row.get("auth_config");
if (authConfigObj == null) return;
Map<String, Object> authConfig;
if (authConfigObj instanceof Map) {
authConfig = new LinkedHashMap<>((Map<String, Object>) authConfigObj);
} else {
return;
}
try {
decryptField(authConfig, "key_value");
decryptField(authConfig, "token");
decryptField(authConfig, "password");
decryptField(authConfig, "client_secret");
row.put("auth_config", authConfig);
} catch (Exception e) {
log.warn("민감 정보 복호화 실패 (암호화되지 않은 데이터일 수 있음)");
}
}
private void encryptField(Map<String, Object> map, String key) {
Object val = map.get(key);
if (val instanceof String str && !str.isBlank()) {
map.put(key, encrypt(str));
}
}
private void decryptField(Map<String, Object> map, String key) {
Object val = map.get(key);
if (val instanceof String str && !str.isBlank()) {
map.put(key, decrypt(str));
}
}
private String encrypt(String plainText) {
try {
byte[] iv = new byte[IV_LENGTH];
new SecureRandom().nextBytes(iv);
byte[] keyBytes = deriveKey();
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE,
new SecretKeySpec(keyBytes, "AES"),
new GCMParameterSpec(GCM_TAG_LENGTH, iv));
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(iv)
+ ":" + HexFormat.of().formatHex(Arrays.copyOfRange(encrypted, encrypted.length - 16, encrypted.length))
+ ":" + HexFormat.of().formatHex(Arrays.copyOf(encrypted, encrypted.length - 16));
} catch (Exception e) {
log.error("암호화 실패", e);
return plainText;
}
}
private String decrypt(String cipherText) {
String[] parts = cipherText.split(":");
if (parts.length != 3) return cipherText;
try {
byte[] iv = HexFormat.of().parseHex(parts[0]);
byte[] authTag = HexFormat.of().parseHex(parts[1]);
byte[] encryptedText = HexFormat.of().parseHex(parts[2]);
byte[] keyBytes = deriveKey();
byte[] combined = new byte[encryptedText.length + authTag.length];
System.arraycopy(encryptedText, 0, combined, 0, encryptedText.length);
System.arraycopy(authTag, 0, combined, encryptedText.length, authTag.length);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE,
new SecretKeySpec(keyBytes, "AES"),
new GCMParameterSpec(GCM_TAG_LENGTH, iv));
return new String(cipher.doFinal(combined), StandardCharsets.UTF_8);
} catch (Exception e) {
log.warn("복호화 실패 (평문 데이터 반환)");
return cipherText;
}
}
private byte[] deriveKey() throws Exception {
KeySpec spec = new PBEKeySpec(encryptionKey.toCharArray(), "salt".getBytes(StandardCharsets.UTF_8), 65536, 256);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
return factory.generateSecret(spec).getEncoded();
}
}