[Spring] spring-vault에서 @VaultPropertySource 여러개 사용하면 안되는 이유
- IT/PROGRAMMING
- 2019. 4. 19. 23:33
스프링 기반의 어플리케이션을 개발할 때, 환경설정으로 보안정보를 다루기 위하여 Vault를 이용하게 된다.
일반적으로 가장 간단하게 Vault를 이용하는 방법은 @VaultPropertySource 또는 @VaultPropertySources 어노테이션을 사용하는 것이다.
import org.springframework.context.annotation.Configuration;
import org.springframework.vault.annotation.VaultPropertySource;
@Configuration
@VaultPropertySource(value = {
"secret/hippolab/wallet1",
"secret/hippolab/wallet2",
"secret/hippolab/wallet3",
"secret/hippolab/wallet4"
})
public class VaultConfig {
}
위 샘플코드에서는 wallet1, wallet2, wallet3, wallet4에 선언된 모든 환경설정은 어플리케이션 로딩시에 스프링 MutablePropertySources에 포함되어 ${vault.properties1}과 같이 쉽게 사용할 수 있다.
그러나 어플리케이션을 만들다보면 여러개의 @VaultPropertySource 어노테이션을 서로 다른 파일에 작성해야할 경우가 생긴다.
이럴때는 아래와 같이 VaultConfig1.java와 VaultConfig2.java 파일에 나눠서 코딩할 것이다.
import org.springframework.context.annotation.Configuration;
import org.springframework.vault.annotation.VaultPropertySource;
@Configuration
@VaultPropertySource(value = {
"secret/hippolab/wallet1",
"secret/hippolab/wallet2",
"secret/hippolab/wallet3",
"secret/hippolab/wallet4"
})
public class VaultConfig1 {
}
import org.springframework.context.annotation.Configuration;
import org.springframework.vault.annotation.VaultPropertySource;
@Configuration
@VaultPropertySource(value = {
"secret/hippolab/wallet5",
"secret/hippolab/wallet6"
})
public class VaultConfig2 {
}
이렇게하고 어플리케이션을 실행하면 wallet1, wallet2, wallet3, wallet4, wallet5, wallet6의 모든 환경설정이 포함되지 않은 것을 확인할 수 있다.
VaultConfig1과 VaultConfig2가 처리되는 순서에 따라 다르겠지만, wallet1,2 또는 wallet3,4의 환경설정이 불러지지 않았을 것이다.
왜 그럴까?
원인은 VaultPropertySourceRegistrar.java 파일안에 있다.
참고로 spring-vault-core 버전 1.1.3.RELEASE 로 설명하겠다.
/*
* Copyright 2016-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.vault.annotation;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.vault.annotation.VaultPropertySource.Renewal;
import org.springframework.vault.core.lease.domain.RequestedSecret;
import org.springframework.vault.core.util.PropertyTransformer;
import org.springframework.vault.core.util.PropertyTransformers;
/**
* Registrar to register {@link org.springframework.vault.core.env.VaultPropertySource}s
* based on {@link VaultPropertySource}.
* <p>
* This class registers potentially multiple property sources based on different Vault
* paths. {@link org.springframework.vault.core.env.VaultPropertySource}s are resolved and
* added to {@link ConfigurableEnvironment} once the bean factory is post-processed. This
* allows injection of Vault properties and and lookup using the
* {@link org.springframework.core.env.Environment}.
*
* @author Mark Paluch
*/
class VaultPropertySourceRegistrar implements ImportBeanDefinitionRegistrar,
BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
ConfigurableEnvironment env = beanFactory.getBean(ConfigurableEnvironment.class);
MutablePropertySources propertySources = env.getPropertySources();
registerPropertySources(
beanFactory.getBeansOfType(
org.springframework.vault.core.env.VaultPropertySource.class)
.values(), propertySources);
registerPropertySources(
beanFactory
.getBeansOfType(
org.springframework.vault.core.env.LeaseAwareVaultPropertySource.class)
.values(), propertySources);
}
private void registerPropertySources(
Collection<? extends PropertySource<?>> propertySources,
MutablePropertySources mutablePropertySources) {
for (PropertySource<?> vaultPropertySource : propertySources) {
if (propertySources.contains(vaultPropertySource.getName())) {
continue;
}
mutablePropertySources.addLast(vaultPropertySource);
}
}
@Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata,
BeanDefinitionRegistry registry) {
Assert.notNull(annotationMetadata, "AnnotationMetadata must not be null!");
Assert.notNull(registry, "BeanDefinitionRegistry must not be null!");
registry.registerBeanDefinition("VaultPropertySourceRegistrar",
BeanDefinitionBuilder //
.rootBeanDefinition(VaultPropertySourceRegistrar.class) //
.setRole(BeanDefinition.ROLE_INFRASTRUCTURE) //
.getBeanDefinition());
Set<AnnotationAttributes> propertySources = attributesForRepeatable(
annotationMetadata, VaultPropertySources.class.getName(),
VaultPropertySource.class.getName());
int counter = 0;
for (AnnotationAttributes propertySource : propertySources) {
String[] paths = propertySource.getStringArray("value");
String ref = propertySource.getString("vaultTemplateRef");
String propertyNamePrefix = propertySource.getString("propertyNamePrefix");
Renewal renewal = propertySource.getEnum("renewal");
Assert.isTrue(paths.length > 0,
"At least one @VaultPropertySource(value) location is required");
Assert.hasText(ref,
"'vaultTemplateRef' in @EnableVaultPropertySource must not be empty");
PropertyTransformer propertyTransformer = StringUtils
.hasText(propertyNamePrefix) ? PropertyTransformers
.propertyNamePrefix(propertyNamePrefix) : PropertyTransformers.noop();
for (String propertyPath : paths) {
if (!StringUtils.hasText(propertyPath)) {
continue;
}
AbstractBeanDefinition beanDefinition = createBeanDefinition(ref,
renewal, propertyTransformer, propertyPath);
registry.registerBeanDefinition("vaultPropertySource#" + counter,
beanDefinition);
counter++;
}
}
}
private AbstractBeanDefinition createBeanDefinition(String ref, Renewal renewal,
PropertyTransformer propertyTransformer, String propertyPath) {
BeanDefinitionBuilder builder;
if (isRenewable(renewal)) {
builder = BeanDefinitionBuilder
.rootBeanDefinition(org.springframework.vault.core.env.LeaseAwareVaultPropertySource.class);
RequestedSecret requestedSecret = renewal == Renewal.ROTATE ? RequestedSecret
.rotating(propertyPath) : RequestedSecret.renewable(propertyPath);
builder.addConstructorArgValue(propertyPath);
builder.addConstructorArgReference("secretLeaseContainer");
builder.addConstructorArgValue(requestedSecret);
}
else {
builder = BeanDefinitionBuilder
.rootBeanDefinition(org.springframework.vault.core.env.VaultPropertySource.class);
builder.addConstructorArgValue(propertyPath);
builder.addConstructorArgReference(ref);
builder.addConstructorArgValue(propertyPath);
}
builder.addConstructorArgValue(propertyTransformer);
builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
return builder.getBeanDefinition();
}
private boolean isRenewable(Renewal renewal) {
return renewal == Renewal.RENEW || renewal == Renewal.ROTATE;
}
@SuppressWarnings("unchecked")
static Set<AnnotationAttributes> attributesForRepeatable(AnnotationMetadata metadata,
String containerClassName, String annotationClassName) {
Set<AnnotationAttributes> result = new LinkedHashSet<AnnotationAttributes>();
addAttributesIfNotNull(result,
metadata.getAnnotationAttributes(annotationClassName, false));
Map<String, Object> container = metadata.getAnnotationAttributes(
containerClassName, false);
if (container != null && container.containsKey("value")) {
for (Map<String, Object> containedAttributes : (Map<String, Object>[]) container
.get("value")) {
addAttributesIfNotNull(result, containedAttributes);
}
}
return Collections.unmodifiableSet(result);
}
private static void addAttributesIfNotNull(Set<AnnotationAttributes> result,
Map<String, Object> attributes) {
if (attributes != null) {
result.add(AnnotationAttributes.fromMap(attributes));
}
}
}
@VaultPropertySource 어노테이션 하나씩마다 VaultPropertySourceRegistrar가 수행되며, registerBeanDefinitions()에 의해서 @VaultPropertySource 어노테이션으로 선언된 wallet들을 하나씩 VaultPropertySource 빈으로 등록된다.
이때 여러 파일에 있는 @VaultPropertySource 어노테이션을 한방에 참조하여 한방에 등록되는 것이 아니라, 각각의 @VaultPropertySource 어노테이션별로 VaultPropertySourceRegistrar가 수행된다.
registry.registerBeanDefinition("vaultPropertySource#" + counter, beanDefinition);
이 부분에서 빈으로 등록되는데 위 샘플 VaultConfig1에 정의된 wallet1, 2, 3, 4는 vaultPropertySource#1, vaultPropertySource#2, vaultPropertySource#3, vaultPropertySource#4 빈으로 등록된다.
그후에 다시 VaultPropertySourceRegistrar가 수행되며 VaultConig2에 정의된 wallet5, 6은 vaultPropertySource#1, vaultPropertySource#2 빈으로 등록된다.
이렇게되면 VaultConfig1에서 먼저 등록된 vaultPropertySource#1, vaultPropertySource#2는 overwriting 된다.
VaultConfig1과 VaultConfig2가 로딩되는 순서 차이는 있겠지만, 어쨌든 우리가 원했던 2개 wallet은 등록되지 않는다.
그럼 어떻게 하면 될까?
결론은 @VaultPropertySource 어노테이션을 여러개 사용할 수 없다는 것이다.
하지만 서로 다른 Java 소스파일에 최대한 @VaultPropertySource 어노테이션 처럼 간단하게 Vault 설정하는 방법은 아래와 같다.
import org.springframework.context.annotation.Configuration;
import org.springframework.vault.annotation.VaultPropertySource;
@Configuration
@VaultPropertySource(value = {
"secret/hippolab/wallet1",
"secret/hippolab/wallet2",
"secret/hippolab/wallet3",
"secret/hippolab/wallet4"
})
public class VaultConfig3 {
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.vault.core.VaultTemplate;
import org.springframework.vault.core.env.VaultPropertySource;
@Configuration
public class VaultConfig4 {
@Bean
public VaultPropertySource wallet5VaultPropertySource(VaultTemplate vaultTemplate) {
return new org.springframework.vault.core.env.VaultPropertySource(vaultTemplate, "secret/hippolab/wallet5");
}
@Bean
public VaultPropertySource wallet6VaultPropertySource(VaultTemplate vaultTemplate) {
return new org.springframework.vault.core.env.VaultPropertySource(vaultTemplate, "secret/hippolab/wallet6");
}
}
여기서는 VaultConfig3.java 기존처럼 @VaultPropertySource 어노테이션을 이용하고, VaultConfig4.java는 원하는 wallet에 해당하는 VaultPropertySource 빈을 직접 생성한다.
이럴경우에 VaultConig3의 @VaultPropertySource 어노테이션에 의해서 VaultPropertySourceRegistrar가 수행되고 postProcessBeanFactory()와 registerPropertySources()에서 등록된 모든 VaultPropertySource 빈을 찾아서 MutablePropertySources에 추가한다.
VaultConfig1과 VaultConfig2의 @VaultPropertySource 어노테이션 처럼 overwriting되는 VaultPropertySource 빈이 없다.
참고로 아래는 Vault 초기화하는 코드이다.
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.vault.annotation.VaultPropertySource;
import org.springframework.vault.config.EnvironmentVaultConfiguration;
/**
* Vault 초기화
*/
@Configuration
@Import(EnvironmentVaultConfiguration.class)
@VaultPropertySource(value = "")
public class InitializeVaultConfig {
}
값이 빈 @VaultPropertySource 어노테이션을 선언한 이유는 만약 VaultConfig3이 없고 VaultConfig4만 사용할 경우에도 VaultPropertySource 빈 MutablePropertySources에 추가되기 위함이다.
VaultPropertySourceRegistrar는 @VaultPropertySource 어노테이션에 의해서 수행되기 때문이다.
'IT > PROGRAMMING' 카테고리의 다른 글
Kotlin의 모든 클래스에서 logger 객체를 편하게 얻을 수 있는 방법 (1) | 2021.08.18 |
---|---|
[Spring] spring-boot 2.1(SpringFramework 5.1)에서 없어진 기능, JSONP 간단하게 구현하기 (0) | 2019.06.09 |
YouTube(유튜브) 동영상의 썸네일 이미지 추출하는 방법 (0) | 2019.04.09 |
[Java] Gson, Jackson(ObjectMapper)으로 JSON 문자열 출력할 때, pretty printing하는 방법 (0) | 2019.04.08 |
[Spring] ConcurrentKafkaListenerContainerFactoryConfigurer를 사용하고 싶다. (0) | 2019.03.14 |