diff --git a/.gitignore b/.gitignore index 549e00a..843d938 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,9 @@ build/ ### VS Code ### .vscode/ + +### Key Files ### +*.p12 + +### yaml ### +application-local.yml \ No newline at end of file diff --git a/pom.xml b/pom.xml index 5514160..bafe3fc 100644 --- a/pom.xml +++ b/pom.xml @@ -104,7 +104,12 @@ org.springframework.cloud spring-cloud-starter-netflix-eureka-client - + + junit + junit + test + + diff --git a/src/main/java/com/t3t/authenticationapi/AuthenticationApiApplication.java b/src/main/java/com/t3t/authenticationapi/AuthenticationApiApplication.java index cfcaebf..118386f 100644 --- a/src/main/java/com/t3t/authenticationapi/AuthenticationApiApplication.java +++ b/src/main/java/com/t3t/authenticationapi/AuthenticationApiApplication.java @@ -2,9 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; - +@ConfigurationPropertiesScan @SpringBootApplication @EnableDiscoveryClient public class AuthenticationApiApplication { diff --git a/src/main/java/com/t3t/authenticationapi/config/RestTemplateConfig.java b/src/main/java/com/t3t/authenticationapi/config/RestTemplateConfig.java new file mode 100644 index 0000000..a5dbf2a --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/config/RestTemplateConfig.java @@ -0,0 +1,52 @@ +package com.t3t.authenticationapi.config; + +import com.t3t.authenticationapi.keymanager.properties.SecretKeyManagerProperties; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.ssl.SSLContextBuilder; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.security.*; +import java.security.cert.CertificateException; +import java.time.Duration; + + +@Configuration +public class RestTemplateConfig { + + /** + * Secret Key Manager 인증서를 사용하여 요청을 보내기 위한 RestTemplate 빈 등록 + * @author woody35545(구건모) + */ + @Bean + @Profile("!local") + public RestTemplate sslRestTemplate(SecretKeyManagerProperties secretKeyManagerProperties) + throws KeyStoreException, IOException, UnrecoverableKeyException, NoSuchAlgorithmException, KeyManagementException, CertificateException { + + KeyStore keyStore = KeyStore.getInstance(secretKeyManagerProperties.getCertKeyType()); + + keyStore.load(secretKeyManagerProperties.getCertKey().getInputStream(), + secretKeyManagerProperties.getPassword().toCharArray()); + + RestTemplate sslRestTemplate = new RestTemplateBuilder() + .setConnectTimeout(Duration.ofSeconds(5)) + .setConnectTimeout(Duration.ofSeconds(5)) + .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) + .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) + .build(); + + sslRestTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(HttpClients.custom() + .setSSLSocketFactory(new SSLConnectionSocketFactory(SSLContextBuilder.create() + .loadKeyMaterial(keyStore, secretKeyManagerProperties.getPassword().toCharArray()).build())).build())); + + return sslRestTemplate; + } +} diff --git a/src/main/java/com/t3t/authenticationapi/exception/SecretKeyManagerApiRequestFailedException.java b/src/main/java/com/t3t/authenticationapi/exception/SecretKeyManagerApiRequestFailedException.java new file mode 100644 index 0000000..a2d9aa7 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/exception/SecretKeyManagerApiRequestFailedException.java @@ -0,0 +1,10 @@ +package com.t3t.authenticationapi.exception; + +/** + * Secret Key Manager API 요청이 실패한 경우 발생하는 예외 + */ +public class SecretKeyManagerApiRequestFailedException extends RuntimeException{ + public SecretKeyManagerApiRequestFailedException(String message) { + super(message); + } +} diff --git a/src/main/java/com/t3t/authenticationapi/keymanager/model/response/SecretKeyManagerResponse.java b/src/main/java/com/t3t/authenticationapi/keymanager/model/response/SecretKeyManagerResponse.java new file mode 100644 index 0000000..b6736f4 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/keymanager/model/response/SecretKeyManagerResponse.java @@ -0,0 +1,25 @@ +package com.t3t.authenticationapi.keymanager.model.response; + +import lombok.Getter; + +/** + * Secret Key Manager API의 응답 형식을 정의한 클래스 + * @author woody35545(구건모) + */ +@Getter +public class SecretKeyManagerResponse { + private SecretKeyManagerResponseHeaderPartDto header; + private SecretKeyManagerResponseBodyPartDto body; + + @Getter + public static class SecretKeyManagerResponseHeaderPartDto { + private int resultCode; + private String resultMessage; + private String isSuccessful; + } + + @Getter + public static class SecretKeyManagerResponseBodyPartDto { + private String secret; + } +} diff --git a/src/main/java/com/t3t/authenticationapi/keymanager/properties/SecretKeyManagerProperties.java b/src/main/java/com/t3t/authenticationapi/keymanager/properties/SecretKeyManagerProperties.java new file mode 100644 index 0000000..89329b9 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/keymanager/properties/SecretKeyManagerProperties.java @@ -0,0 +1,25 @@ +package com.t3t.authenticationapi.keymanager.properties; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Profile; +import org.springframework.core.io.Resource; + +/** + * Secret Key Manager 에서 사용될 속성을 저장하는 프로퍼티 클래스 + * @author woody35545(구건모) + */ +@Getter +@Setter +@Profile("!local") +@ConfigurationProperties(prefix = "t3t.secret-key-manager") +public class SecretKeyManagerProperties { + private String appKey; + private String password; + private String certKeyType; + private String certKeyPath; + @Value("${t3t.secretKeyManager.certKeyPath}") + private Resource certKey; +} \ No newline at end of file diff --git a/src/main/java/com/t3t/authenticationapi/keymanager/properties/SecretKeyProperties.java b/src/main/java/com/t3t/authenticationapi/keymanager/properties/SecretKeyProperties.java new file mode 100644 index 0000000..8b39d03 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/keymanager/properties/SecretKeyProperties.java @@ -0,0 +1,34 @@ +package com.t3t.authenticationapi.keymanager.properties; + +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +/** + * Secret Key Manager 에 등록된 기밀 데이터의 key id를 저장하는 프로퍼티 클래스 + * @author woody35545(구건모) + */ +@Profile("!local") +@Getter +@Component +public class SecretKeyProperties { + @Value("${t3t.secretKeyManager.secrets.databaseServerIpAddress.keyId}") + private String databaseIpAddressKeyId; + @Value("${t3t.secretKeyManager.secrets.databaseServerPort.keyId}") + private String databasePortKeyId; + @Value("${t3t.secretKeyManager.secrets.databaseServerUsername.keyId}") + private String databaseNameKeyId; + @Value("${t3t.secretKeyManager.secrets.databaseName.keyId}") + private String databaseUsernameKeyId; + @Value("${t3t.secretKeyManager.secrets.databaseServerPassword.keyId}") + private String databasePasswordKeyId; + @Value("${t3t.secretKeyManager.secrets.jwtSecretKey.keyId}") + private String jwtSecretKeyId; + @Value("${t3t.secretKeyManager.secrets.redisServerIpAddress.keyId}") + private String redisIpAddressKeyId; + @Value("${t3t.secretKeyManager.secrets.redisServerPort.keyId}") + private String redisPortKeyId; + @Value("${t3t.secretKeyManager.secrets.redisServerPassword.keyId}") + private String redisPasswordKeyId; +} diff --git a/src/main/java/com/t3t/authenticationapi/keymanager/service/SecretKeyManagerService.java b/src/main/java/com/t3t/authenticationapi/keymanager/service/SecretKeyManagerService.java new file mode 100644 index 0000000..c54b0c7 --- /dev/null +++ b/src/main/java/com/t3t/authenticationapi/keymanager/service/SecretKeyManagerService.java @@ -0,0 +1,59 @@ +package com.t3t.authenticationapi.keymanager.service; + +import com.t3t.authenticationapi.exception.SecretKeyManagerApiRequestFailedException; +import com.t3t.authenticationapi.keymanager.model.response.SecretKeyManagerResponse; +import com.t3t.authenticationapi.keymanager.properties.SecretKeyManagerProperties; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpMethod; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +/** + * Secret Key Manager 에 등록된 Secret 값을 가져오기 위한 서비스 클래스 + * + * @author woody35545(구건모) + */ +@Profile("!local") +@Slf4j +@Service +@RequiredArgsConstructor +public class SecretKeyManagerService { + private final RestTemplate sslRestTemplate; + private final SecretKeyManagerProperties secretKeyManagerProperties; + + private static final ParameterizedTypeReference secretKeyManagerResponseTypeReference + = new ParameterizedTypeReference() { + }; + + /** + * Secret Key Manager 에서 Secret 값 조회 + * + * @param keyId 조회할 Key ID(Secret Key Manager 에 등록된 기밀 데이터의 Key ID) + * @return Secret Key Manager 에서 조회한 Secret 값을 String 형태로 반환 + * @author woody35545(구건모) + */ + public String getSecretValue(String keyId) { + + HttpEntity response = + sslRestTemplate.exchange("https://api-keymanager.nhncloudservice.com/keymanager/v1.0/appkey/{appKey}/secrets/{keyId}", + HttpMethod.GET, null, SecretKeyManagerResponse.class, + secretKeyManagerProperties.getAppKey(), keyId); + + SecretKeyManagerResponse responseBody = response.getBody(); + + if (responseBody == null) { + throw new SecretKeyManagerApiRequestFailedException("Response body is null."); + } + + if (responseBody.getHeader() == null || responseBody.getBody() == null || !responseBody.getHeader().getIsSuccessful().equals("true") || responseBody.getBody().getSecret() == null) { + log.error("Secret Key Manager API response: {}", responseBody); + throw new SecretKeyManagerApiRequestFailedException(new StringBuilder().append("Fail to request Secret Key Manager API (Key ID:").append(keyId).append(")").toString()); + } + + return responseBody.getBody().getSecret(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 628f4d4..5beb8db 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -27,4 +27,31 @@ spring: logging: level: - org.hibernate.SQL: debug \ No newline at end of file + org.hibernate.SQL: debug + +t3t: + secretKeyManager: + certKeyPath: ${secretKeyManagerCertKeyPath} + certKeyType: ${secretKeyManagerCertKeyType} + appKey: ${secretKeyManagerAppKey} + password: ${secretKeyManagerPassword} + + secrets: + databaseName: + keyId: "e3203972cbf04433b90c752f695d5736" + databaseServerIpAddress: + keyId: "62911d2c30064812b2b2c97a8dd90782" + databaseServerPort: + keyId: "48e191996aa748938a1edb62652336f4" + databaseServerUsername: + keyId: "f008c1d3f87f4f88ae57bd03871eb10d" + databaseServerPassword: + keyId: "8a65684780224384a681c3e9035ca7d6" + jwtSecretKey: + keyId: "e4f4d4a87ccd49e594f03dffee9fa58d" + redisServerIpAddress: + keyId: "10ee8b6140cc49ffa9e7a7c8a2924a3e" + redisServerPort: + keyId: "0582f8b117604b7d86e9f3ff26931cde" + redisServerPassword: + keyId: "ec1eb8e0706e402cbec8487cbcb86564" diff --git a/src/test/java/com/t3t/authenticationapi/keymanager/SecretKeyManagerTest.java b/src/test/java/com/t3t/authenticationapi/keymanager/SecretKeyManagerTest.java new file mode 100644 index 0000000..c982a35 --- /dev/null +++ b/src/test/java/com/t3t/authenticationapi/keymanager/SecretKeyManagerTest.java @@ -0,0 +1,169 @@ +package com.t3t.authenticationapi.keymanager; + +import com.t3t.authenticationapi.keymanager.properties.SecretKeyProperties; +import com.t3t.authenticationapi.keymanager.service.SecretKeyManagerService; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; + +/** + * Secret Key Manager API 를 통해 기밀 데이터를 정상적으로 가져오는지에 대한 통합 테스트한다.
+ * 해당 테스트는 가져온 기밀데이터가 실제값과 동일한지 확인하는 테스트가 아니라,
+ * Secret Key Manager API 를 통해 값이 조회되는지에 대한 테스트이다.
+ * Secret Key Manager 빈을 위한 실행 환경 변수를 설정하고, application.yml 파일을 통해 테스트에 사용할 key id 를 정의하여 테스트한다.
+ * @apiNote 확인이 필요한 경우에만 실행하도록 설정하기 위해 테스트를 하고자하는 항목을 제외하고는 @Disabled 어노테이션을 선언하여 테스트를 비활성화한다.
+ * @see SecretKeyManagerService + * @see SecretKeyProperties + * @author woody35545(구건모) + */ +@SpringBootTest +@TestPropertySource(locations = "classpath:application.yml") +@Slf4j +class SecretKeyManagerTest { + + @Autowired + private SecretKeyProperties secretKeyProperties; + + @Autowired + private SecretKeyManagerService secretKeyManagerService; + + + /** + * Secret Key Manager API를 통해 databaseIpAddress 값이 정상적으로 로드되는지 테스트 + * @author woody35545(구건모) + */ + @Test + @Disabled + void databaseIpAddressLoadTest () { + + // when & then + Assertions.assertDoesNotThrow(()-> secretKeyManagerService.getSecretValue(secretKeyProperties.getDatabaseIpAddressKeyId())); + String value = secretKeyManagerService.getSecretValue(secretKeyProperties.getDatabaseIpAddressKeyId()); + Assertions.assertNotNull(value); + + log.info("databaseIpAddress => {}", value); + } + + /** + * Secret Key Manager API를 통해 databasePort 값이 정상적으로 로드되는지 테스트 + * @author woody35545(구건모) + */ + @Test + @Disabled + void databasePortLoadTest () { + + // when & then + Assertions.assertDoesNotThrow(() -> secretKeyManagerService.getSecretValue(secretKeyProperties.getDatabasePortKeyId())); + String value = secretKeyManagerService.getSecretValue(secretKeyProperties.getDatabasePortKeyId()); + Assertions.assertNotNull(value); + + log.info("databasePort => {}", value); + } + + /** + * Secret Key Manager API를 통해 databaseName 값이 정상적으로 로드되는지 테스트 + * @author woody35545(구건모) + */ + @Test + @Disabled + void databaseNameLoadTest () { + + // when & then + Assertions.assertDoesNotThrow(() -> secretKeyManagerService.getSecretValue(secretKeyProperties.getDatabaseNameKeyId())); + String value = secretKeyManagerService.getSecretValue(secretKeyProperties.getDatabaseNameKeyId()); + Assertions.assertNotNull(value); + + log.info("databaseName => {}", value); + } + + /** + * Secret Key Manager API를 통해 databaseUsername 값이 정상적으로 로드되는지 테스트 + * @author woody35545(구건모) + */ + @Test + @Disabled + void databaseUsernameLoadTest () { + + // when & then + Assertions.assertDoesNotThrow(() -> secretKeyManagerService.getSecretValue(secretKeyProperties.getDatabaseUsernameKeyId())); + String value = secretKeyManagerService.getSecretValue(secretKeyProperties.getDatabaseUsernameKeyId()); + Assertions.assertNotNull(value); + + log.info("databaseUsername => {}", value); + } + + + /** + * Secret Key Manager API를 통해 databasePassword 값이 정상적으로 로드되는지 테스트 + * @author woody35545(구건모) + */ + @Test + @Disabled + void databasePasswordLoadTest () { + Assertions.assertDoesNotThrow(() -> secretKeyManagerService.getSecretValue(secretKeyProperties.getDatabasePasswordKeyId())); + String value = secretKeyManagerService.getSecretValue(secretKeyProperties.getDatabasePasswordKeyId()); + + Assertions.assertNotNull(value); + } + + + /** + * Secret Key Manager API를 통해 jwtSecretKey 값이 정상적으로 로드되는지 테스트 + * @author woody35545(구건모) + */ + @Test + @Disabled + void jwtSecretKeyLoadTest () { + Assertions.assertDoesNotThrow(() -> secretKeyManagerService.getSecretValue(secretKeyProperties.getJwtSecretKeyId())); + String value = secretKeyManagerService.getSecretValue(secretKeyProperties.getJwtSecretKeyId()); + + Assertions.assertNotNull(value); + } + + /** + * Secret Key Manager API 를 통해 redisIpAddress 값이 정상적으로 로드되는지 테스트 + * @author woody35545(구건모) + */ + @Test + @Disabled + void redisIpAddressLoadTest() { + Assertions.assertDoesNotThrow(() -> secretKeyManagerService.getSecretValue(secretKeyProperties.getRedisIpAddressKeyId())); + + String value = secretKeyManagerService.getSecretValue(secretKeyProperties.getRedisIpAddressKeyId()); + Assertions.assertNotNull(value); + + log.info("redisIpAddress => {}", value); + } + + /** + * Secret Key Manager API 를 통해 redisPort 값이 정상적으로 로드되는지 테스트 + * @author woody35545(구건모) + */ + @Test + @Disabled + void redisPortLoadTest() { + Assertions.assertDoesNotThrow(() -> secretKeyManagerService.getSecretValue(secretKeyProperties.getRedisPortKeyId())); + + String value = secretKeyManagerService.getSecretValue(secretKeyProperties.getRedisPortKeyId()); + Assertions.assertNotNull(value); + + log.info("redisPort => {}", value); + } + + /** + * Secret Key Manager API 를 통해 redisPassword 값이 정상적으로 로드되는지 테스트 + * @author woody35545(구건모) + */ + @Test + @Disabled + void redisPasswordLoadTest() { + Assertions.assertDoesNotThrow(() -> secretKeyManagerService.getSecretValue(secretKeyProperties.getRedisPasswordKeyId())); + + String value = secretKeyManagerService.getSecretValue(secretKeyProperties.getRedisPasswordKeyId()); + Assertions.assertNotNull(value); + } +} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 5e12484..ff6706f 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -31,3 +31,30 @@ eureka: service-url: defaultZone : http://127.0.0.1:8761/eureka + +t3t: + secretKeyManager: + certKeyPath: ${secretKeyManagerCertKeyPath} + certKeyType: ${secretKeyManagerCertKeyType} + appKey: ${secretKeyManagerAppKey} + password: ${secretKeyManagerPassword} + + secrets: + databaseName: + keyId: "e3203972cbf04433b90c752f695d5736" + databaseServerIpAddress: + keyId: "62911d2c30064812b2b2c97a8dd90782" + databaseServerPort: + keyId: "48e191996aa748938a1edb62652336f4" + databaseServerUsername: + keyId: "f008c1d3f87f4f88ae57bd03871eb10d" + databaseServerPassword: + keyId: "8a65684780224384a681c3e9035ca7d6" + jwtSecretKey: + keyId: "e4f4d4a87ccd49e594f03dffee9fa58d" + redisServerIpAddress: + keyId: "10ee8b6140cc49ffa9e7a7c8a2924a3e" + redisServerPort: + keyId: "0582f8b117604b7d86e9f3ff26931cde" + redisServerPassword: + keyId: "ec1eb8e0706e402cbec8487cbcb86564"