明明环境变量已经解密,为啥@ConfigurationProperties 注入还是加密值?
问题背景
在微服务的 application.properties 文件中有一个 test.container-name 配置。原始配置如下:
test.container-name=Tomcat
同时有一个 Java 类 TestConfigProperty 中通过 @ConfigurationProperties 注解注入这个配置属性到它的变量 containerName 中,代码如下:
@ConfigurationProperties(prefix = "test")
@Component
public class TestConfigProperty {
private String containerName;
public String getContainerName() {
return containerName;
}
public void setContainerName(String containerName) {
this.containerName = containerName;
}
}
现在因为 test.container-name 配置包含敏感信息,不能直接配置原始的值,需要配置加密之后的值,在微服务启动的时候解密。现在是 test.container-name 配置引用了 TEST_CONTAINER_NAME 环境变量。配置如下:
test.container-name=${TEST_CONTAINER_NAME}
然后在环境变量中配置了加密之后的值。在本案例中为了简化,这里加密就用的 Base64 编码作为示例演示。如下图所示:


在项目中有框架提供了在微服务启动时对加密后的字符串解密的能力,实现的基本原理是提供了一个 DecryptEnvironmentPostProcessor 类扩展了 EnvironmentPostProcessor。
在它的 postProcessEnvironment() 方法中,判断环境变量配置的值是否是以 ENC_ 开头,如果是则进行解密。解密之后放到一个 MapPropertySource 里面,然后添加到所有的 PropertySource 的前面。示例代码如下:
public class DecryptEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
private static final String DECRYPTED_SOURCE_NAME = "decryptedSystemEnvironment";
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
String systemEnvName = StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME;
MapPropertySource systemEnvSource = (MapPropertySource) environment.getPropertySources().get(systemEnvName);
Map decryptedMap = new HashMap<>();
if (systemEnvSource == null) {
return;
}
systemEnvSource.getSource().forEach((key, value) -> {
if (value instanceof String strVal) {
// 这里进行了解密
if (StringUtils.isNotEmpty(strVal) && strVal.startsWith("ENC_")) {
String plainText = new String(Base64.getDecoder().decode(strVal.substring(4)));
decryptedMap.put(key, plainText);
}
}
});
if (!decryptedMap.isEmpty()) {
MapPropertySource decryptedSource = new MapPropertySource(DECRYPTED_SOURCE_NAME, decryptedMap);
// 这里添加到所有的PropertySource的前面
environment.getPropertySources().addBefore(systemEnvName, decryptedSource);
}
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
按照上述配置,通过调试发现类 TestConfigProperty 里面注入的还是加密之后的值,而并不是想要的解密之后的值。如下图所示:

查看 Environment 的 getPropertySources() 方法的返回值中,解密之后的环境变量属性配置确实是在未解密的环境变量属性配置之前,按照直观上的理解,那应该注入的是解密之后的值才对,但是实际结果却不是这样的。如下图所示:

问题原理
之前的文章这就是宽松的适配规则!里面讲了宽松适配的原理。在 Spring 的框架体系中是在 ConfigurationPropertiesBindingPostProcessor 中的 postProcessBeforeInitialization() 中实现对有 @ConfigurationProperties 注解修饰类的属性进行绑定的。
在它的内部实际上是通过调用 ConfigurationPropertiesBinder 的 bind() 来实现属性绑定的。代码如下:
public class ConfigurationPropertiesBindingPostProcessor
implements BeanPostProcessor, PriorityOrdered, ApplicationContextAware, InitializingBean {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (!hasBoundValueObject(beanName)) {
bind(ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName));
}
return bean;
}
private void bind(ConfigurationPropertiesBean bean) {
if (bean == null) {
return;
}
Assert.state(bean.asBindTarget().getBindMethod() != BindMethod.VALUE_OBJECT,
"Cannot bind @ConfigurationProperties for bean '" + bean.getName()
+ "'. Ensure that @ConstructorBinding has not been applied to regular bean");
try {
// 这里实际上是调用了ConfigurationPropertiesBinder的bind()方法
this.binder.bind(bean);
}
catch (Exception ex) {
throw new ConfigurationPropertiesBindException(bean, ex);
}
}
}

在 ConfigurationPropertiesBinder 的 bind() 方法又调用了 Binder 的 bind() 方法。如下图所示:

在调用 Binder 的 bind() 方法时,会把注解上配置的前缀传进去,在本案例中就是 test,并基于这个前缀创建一个 ConfigurationPropertyName 对象,然后最终调用到 bindObject() 方法。代码如下:
public class Binder {
public BindResult bind(String name, Bindable target, BindHandler handler) {
// 这里基于test前缀创建了ConfigurationPropertyName对象
return bind(ConfigurationPropertyName.of(name), target, handler);
}
private T bind(ConfigurationPropertyName name, Bindable target, BindHandler handler, Context context,
boolean allowRecursiveBinding, boolean create) {
try {
Bindable replacementTarget = handler.onStart(name, target, context);
if (replacementTarget == null) {
return handleBindResult(name, target, handler, context, null, create);
}
target = replacementTarget;
// 调用bindObject()方法
Object bound = bindObject(name, target, handler, context, allowRecursiveBinding);
return handleBindResult(name, target, handler, context, bound, create);
}
catch (Exception ex) {
return handleBindError(name, target, handler, context, ex);
}
}
}
在 bindObject() 中首先调用 findProperty() 方法查找属性,因为当前只是前缀 test,因此肯定是找不到对应的属性配置的。 因此往下走会调用到 bindDataObject()方法。对于 JavaBean 来说,在 Binder 的 bindDataObject() 方法最终会调用到 JavaBeanBinder 的 bind() 方法。代码如下:
public class Binder {
private Object bindObject(ConfigurationPropertyName name, Bindable target, BindHandler handler,
Context context, boolean allowRecursiveBinding) {
ConfigurationProperty property = findProperty(name, target, context);
if (property == null && context.depth != 0 && containsNoDescendantOf(context.getSources(), name)) {
return null;
}
// 省略中间代码
//调用bindDataObject()方法
return bindDataObject(name, target, handler, context, allowRecursiveBinding);
}
private Object bindDataObject(ConfigurationPropertyName name, Bindable target, BindHandler handler,
Context context, boolean allowRecursiveBinding) {
if (isUnbindableBean(name, target, context)) {
return null;
}
Class type = target.getType().resolve(Object.class);
BindMethod bindMethod = target.getBindMethod();
if (!allowRecursiveBinding && context.isBindingDataObject(type)) {
return null;
}
// 注意这里的lambda表达式,在JavaBeanBinder的bind()方法最终又会调用到这个lambda表达式
DataObjectPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(name.append(propertyName),
propertyTarget, handler, context, false, false);
// 这里会调用到JavaBeanBinder的bind()方法
return context.withDataObject(type, () -> fromDataObjectBinders(bindMethod,
(dataObjectBinder) -> dataObjectBinder.bind(name, target, context, propertyBinder)));
}
}
在 JavaBeanBinder 的 bind() 方法中会获取这个对象的所有的 BeanProperty,然后又反调用回 Binder 中的lambda表达式了。代码如下:
class JavaBeanBinder implements DataObjectBinder {
@Override
public T bind(ConfigurationPropertyName name, Bindable target, Context context,
DataObjectPropertyBinder propertyBinder) {
boolean hasKnownBindableProperties = target.getValue() != null && hasKnownBindableProperties(name, context);
Bean bean = Bean.get(target, hasKnownBindableProperties);
if (bean == null) {
return null;
}
BeanSupplier beanSupplier = bean.getSupplier(target);
boolean bound = bind(propertyBinder, bean, beanSupplier, context);
return (bound ? beanSupplier.get() : null);
}
private boolean bind(DataObjectPropertyBinder propertyBinder, Bean bean, BeanSupplier beanSupplier,
Context context) {
boolean bound = false;
for (BeanProperty beanProperty : bean.getProperties().values()) { // 获取这个对象上所有的BeanProperty属性
bound |= bind(beanSupplier, propertyBinder, beanProperty);
context.clearConfigurationProperty();
}
return bound;
}
private boolean bind(BeanSupplier beanSupplier, DataObjectPropertyBinder propertyBinder,
BeanProperty property) {
String propertyName = determinePropertyName(property);
ResolvableType type = property.getType();
Supplier 原文地址: https://www.cveoy.top/t/topic/qFRM 著作权归作者所有。请勿转载和采集!