Jasypt 적용하기 + 이슈
안녕하세요! 이번 포스팅은 제가 jasypt를 사용하고 겪은 이슈에 대해 공유드리려 합니다.
Jasypt?
Jasypt는 홈페이지에서 다음과 같이 설명합니다.
Jasypt is a java library which allows the developer to add basic encryption capabilities to his/her projects with minimum effort, and without the need of having deep knowledge on how cryptography works.
직역해보면 개발자들의 프로젝트를 기본적으로 암호화 해줄수있게 해주고 보안적인 지식이 깊지 않더라도 쉽게 적용할 수 있는 자바 라이브러리 라고 합니다.
사용 방법
우선 build.gradle에 다음과 같이 의존성을 추가해줍니다.
implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.4'
그 후 Configuration을 다음과 같이 등록해줍니다.
@Configuration
@EnableEncryptableProperties
public class JasyptConfig {
@Value("${jasypt.encryptor.password}")
private String encryptKey;
@Bean("jasyptStringEncryptor")
public StringEncryptor stringEncryptor() {
StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
SimpleStringPBEConfig config = new SimpleStringPBEConfig();
config.setPassword(encryptKey);
config.setAlgorithm("PBEWithMD5AndDES");
config.setKeyObtentionIterations("1000");
config.setPoolSize("1");
config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
config.setStringOutputType("base64");
encryptor.setConfig(config);
return encryptor;
}
}
@Value
를 통해 yml에서 주입받음을 알 수 있지만, encryptKey를 application.yml
에 등록하면 암호화를 안하는 것과 마찬가지입니다.
그래서 jar 실행 시 -Djasypt.encryptor.passowrd={직접 키 입력}
을 통해 주입해줘야 합니다.
적용을 하기 전에 yml 설정부터 짚어보겠습니다.
jasypt:
encryptor:
bean: jasyptStringEncryptor
property:
prefix: ENC(
suffix: )
jasypt.encryptor.bean
이곳에서는 @Bean
으로 등록한 StringEncryptor의 Bean name을 넣어줍니다.
jasypt.encryptor.bean.property
jasypt가 인식할 수 있게끔 prefix는 ENC(
, suffix는 )
를 넣어줍니다.
암호화를 주로 사용하는 곳은 애플리케이션의 DB 연결시 필요한 username, password나 외부 라이브러리나 api 연결 시 필요한 정보를 yml에 표기할 때 입니다. 그렇다면 암호화된 값을 어떻게 가져올까요?
저는 테스트 코드를 통해 가져왔습니다. 코드를 보면 바로 이해되실겁니다.
public class JasyptTest {
@Test
void test() {
// given
StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
SimpleStringPBEConfig config = new SimpleStringPBEConfig();
config.setPassword("test");
config.setAlgorithm("PBEWithMD5AndDES");
config.setKeyObtentionIterations("1000");
config.setPoolSize("1");
config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
config.setStringOutputType("base64");
encryptor.setConfig(config);
// when
String testString = "testString";
String encryptedTestString = encryptor.encrypt(testString);
System.out.println(encryptedTestString);
// then
assertEquals(testString, encryptor.decrypt(encryptedTestString));
}
}
given절에는 설정과 똑같은 코드를 가져왔습니다. when절에는 testString을 암호화 후, 이를 복호화한 값이 원문과 맞는지 then절에서 검증했습니다.
print한 암호화문을 검증해보겠습니다.
@Test
void test() {
...
// when
String testString = "testString";
// then
assertEquals(testString, encryptor.decrypt("Z8ICfwBWU3COACibH7CF5iOFg4A0Sxrh"));
}
이렇게 해서 암호화가 잘 되었음을 알 수 있습니다. 이제 암호화된 문자열을 앞에 prefix, suffix에서 설정했던 것 처럼 넣어줍니다.
cloud:
aws:
credentials:
access-key: ENC(vddcKGLGK7uW5FuMCppk/9KrEpqOLD1bv9MwgGcriQE=)
secret-key: ENC(MmqBJCNvh4dUz7oxFIxYhd+i4LUjfXs0KZwOz9+Vfco2WkXU4/boAkG9iwoP1xgjTkhRM3G2fC0=)
이런식으로요. 참고로 이 yml은 제가 진행하고있는 챌린지에서 사진 저장을 위해 S3와 연동하기 위한 access-key, secret-key를 암호화한 설정입니다.
이슈 상황 (S3 연동)
평소같았으면 예시를 데이터베이스 username, password로 보여드렸을 것입니다.
근데 왜 굳이 S3 연동하는 yml 설정값으로 보여드렸을까요?
우선, 저는 S3를 연동할 때 별도의 Configuration 클래스를 만들지 않고 디폴트(SpringBoot의 Autoconfiguration)로 설정했습니다. 하지만 이번 프로젝트에서는 jasypt를 적용했을 때 AutoConfiguration이 병목이 될 줄 몰랐습니다. 같이 보시죠
우선, 기본 설정의 경우에는 애플리케이션을 돌리는데 문제가 없습니다. 문제는 실제 사진 저장(S3를 사용할 때)할 때 입니다.
com.amazonaws.services.s3.model.AmazonS3Exception: The authorization header is malformed; the authorization component "Credential=ENC(vddcKGLGK7uW5FuMCppk/9KrEpqOLD1bv9MwgGcriQE=)/20220122/ap-northeast-2/s3/aws4_request" is malformed. (Service: Amazon S3; Status Code: 400; Error Code: AuthorizationHeaderMalformed; Request ID: 9J3F6501ZENBB3NY; S3 Extended Request ID: TmqLaB/k8P2xcdlzZvnDnVwytbSBzAvJOn/nMXTDvk4qeXrH3HE+ku1SiEKyzR//n/I5+/JB81k=; Proxy: null)
at com.amazonaws.http.AmazonHttpClient$RequestExecutor.handleErrorResponse(AmazonHttpClient.java:1811) ~[aws-java-sdk-core-1.11.792.jar:na]
at com.amazonaws.http.AmazonHttpClient$RequestExecutor.handleServiceErrorResponse(AmazonHttpClient.java:1395) ~[aws-java-sdk-core-1.11.792.jar:na]
at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeOneRequest(AmazonHttpClient.java:1371) ~[aws-java-sdk-core-1.11.792.jar:na]
at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeHelper(AmazonHttpClient.java:1145) ~[aws-java-sdk-core-1.11.792.jar:na]
at com.amazonaws.http.AmazonHttpClient$RequestExecutor.doExecute(AmazonHttpClient.java:802) ~[aws-java-sdk-core-1.11.792.jar:na]
at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeWithTimer(AmazonHttpClient.java:770) ~[aws-java-sdk-core-1.11.792.jar:na]
at com.amazonaws.http.AmazonHttpClient$RequestExecutor.execute(AmazonHttpClient.java:744) ~[aws-java-sdk-core-1.11.792.jar:na]
at com.amazonaws.http.AmazonHttpClient$RequestExecutor.access$500(AmazonHttpClient.java:704) ~[aws-java-sdk-core-1.11.792.jar:na]
at com.amazonaws.http.AmazonHttpClient$RequestExecutionBuilderImpl.execute(AmazonHttpClient.java:686) ~[aws-java-sdk-core-1.11.792.jar:na]
at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:550) ~[aws-java-sdk-core-1.11.792.jar:na]
at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:530) ~[aws-java-sdk-core-1.11.792.jar:na]
at com.amazonaws.services.s3.AmazonS3Client.invoke(AmazonS3Client.java:5062) ~[aws-java-sdk-s3-1.11.792.jar:na]
at com.amazonaws.services.s3.AmazonS3Client.invoke(AmazonS3Client.java:5008) ~[aws-java-sdk-s3-1.11.792.jar:na]
at com.amazonaws.services.s3.AmazonS3Client.access$300(AmazonS3Client.java:394) ~[aws-java-sdk-s3-1.11.792.jar:na]
at com.amazonaws.services.s3.AmazonS3Client$PutObjectStrategy.invokeServiceCall(AmazonS3Client.java:5950) ~[aws-java-sdk-s3-1.11.792.jar:na]
at com.amazonaws.services.s3.AmazonS3Client.uploadObject(AmazonS3Client.java:1812) ~[aws-java-sdk-s3-1.11.792.jar:na]
at com.amazonaws.services.s3.AmazonS3Client.putObject(AmazonS3Client.java:1772) ~[aws-java-sdk-s3-1.11.792.jar:na]
at com.danggn.challenge.common.client.S3StorageClient.upload(S3StorageClient.java:21) ~[classes/:na]
The authorization header is malformed; 라는 메세지와 함께 Credential 값을 복호화시킨 값이 아닌 yml에 적어둔 ENC(...) 그대로 읽어온 것입니다.
그래서 디버깅 모드로 애플리케이션을 띄울 때 BasicAWSCredentials
클래스와 앞에서 설정한 JasyptConfig
의 생성자에 디버깅을 찍어보니 BasicAWSCredentials
클래스가 먼저 빈 주입이 되었습니다.
그래서 AWS 설정 Configuration 클래스를 따로 만든 후 디버깅 포인트를 걸어보았습니다. 클래스는 다음과 같습니다.
@Configuration
public class AmazonS3Config {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
public AmazonS3Client amazonS3Client() {
BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(credentials))
.build();
}
}
이렇게 AmazonS3Config를 만든 후에 BasicAWSCredentials
, JasyptConfig
, AmazonS3Config
생성자에 모두 디버깅 포인트를 걸어 run 해보면
우선 똑같이 BasicAWSCredentials가 디버깅 포인트에 걸립니다. 그 다음은
이렇게 JasyptConfig가 잡힙니다. 마지막으로
이렇게 AmazonS3Config가 잡힙니다. 그러고 나서 이 암호화된 값을 다시
BasicAWSCredentials
클래스에 덮어씌웁니다. 이렇게 해서 복호화된 Credential이 주입됨이 확인되었습니다.