안녕하세요, 하마연구소 입니다.
스프링 기반의 어플리케이션을 개발할 때, 환경설정으로 보안정보를 다루기 위하여 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
, wallet2
또는 wallet3
, wallet4
의 환경설정이 불러지지 않았을 것입니다.
왜 그럴까???
원인은 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
어노테이션에 의해서 수행되기 때문입니다.
감사합니다.