核心内容摘要
Python基于Vue的教师科研管理系统 django flask pycharm
问题背景在微服务的 application.properties 文件中有一个test.container-name配置。
原始配置如下/* by
hk - online tools website :
hk/zh/togif.html */ test.container-nameTomcat同时有一个 Java 类TestConfigProperty中通过ConfigurationProperties注解注入这个配置属性到它的变量containerName中代码如下/* by
hk - online tools website :
hk/zh/togif.html */ 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); MapString, Object 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(Base
getDecoder().decode(strVal.substring(
)); 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 T BindResultT bind(String name, BindableT target, BindHandler handler) { // 这里基于test前缀创建了ConfigurationPropertyName对象 return bind(ConfigurationPropertyName.of(name), target, handler); } private T T bind(ConfigurationPropertyName name, BindableT target, BindHandler handler, Context context, boolean allowRecursiveBinding, boolean create) { try { BindableT 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 T Object bindObject(ConfigurationPropertyName name, BindableT 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 T bind(ConfigurationPropertyName name, BindableT target, Context context, DataObjectPropertyBinder propertyBinder) { boolean hasKnownBindableProperties target.getValue() ! null hasKnownBindableProperties(name, context); BeanT bean Bean.get(target, hasKnownBindableProperties); if (bean null) { return null; } BeanSupplierT beanSupplier bean.getSupplier(target); boolean bound bind(propertyBinder, bean, beanSupplier, context); return (bound ? beanSupplier.get() : null); } private T boolean bind(DataObjectPropertyBinder propertyBinder, BeanT bean, BeanSupplierT beanSupplier, Context context) { boolean bound false; for (BeanProperty beanProperty : bean.getProperties().values()) { // 获取这个对象上所有的BeanProperty属性 bound | bind(beanSupplier, propertyBinder, beanProperty); context.clearConfigurationProperty(); } return bound; } private T boolean bind(BeanSupplierT beanSupplier, DataObjectPropertyBinder propertyBinder, BeanProperty property) { String propertyName determinePropertyName(property); ResolvableType type property.getType(); SupplierObject value property.getValue(beanSupplier); Annotation[] annotations property.getAnnotations(); Object bound propertyBinder.bindProperty(propertyName, //这个地方实际上又反调用回Binder中的lambda表达式了 Bindable.of(type).withSuppliedValue(value).withAnnotations(annotations)); if (bound null) { return false; } if (property.isSettable()) { property.setValue(beanSupplier, bound); } else if (value null || !bound.equals(value.get())) { throw new IllegalStateException(No setter found for property: property.getName()); } return true; } }BeanProperty对象会将 JavaBean 中的属性统一为 Dash 格式。
在本案例中属性名称是containerName统一之后就变成了container-name。
如下图所示在Binder中 lambda 表达式会将属性拼接到已有的ConfigurationPropertyName前缀上在本案例中就变成了test.container-name。
然后又递归调用bind()方法然后又调用findProperty()方法尝试从从对应的ConfigurationPropertySource中获取对应的配置中查找这个属性。
Spring 提供了SpringIterableConfigurationPropertySource作为ConfigurationPropertySource实现类 它实际是对PropertySource的一个适配内部有一个propertySource表示真正的配置。
通过调试contex.getSource()方法的返回值可以看到加密之后的PropertySource确实是在没有加密的前面。
代码如下DataObjectPropertyBinder propertyBinder (propertyName, propertyTarget) - bind(name.append(propertyName), //这里将属性名称拼接到test前缀上 propertyTarget, handler, context, false, false); private T ConfigurationProperty findProperty(ConfigurationPropertyName name, BindableT target, Context context) { if (name.isEmpty() || target.hasBindRestriction(BindRestriction.NO_DIRECT_PROPERTY)) { return null; } for (ConfigurationPropertySource source : context.getSources()) { ConfigurationProperty property source.getConfigurationProperty(name); if (property ! null) { return property; } } return null; }在getConfigurationProperty()方法中首先调用父类SpringConfigurationPropertySource的getConfigurationProperty()方法。
在该方法中会调用PropertyMapper的map()方法对传入的ConfigurationPropertyName类型的name进行转换然后根据转换后拿到的名称去PropertySource中获取对应的属性。
class SpringConfigurationPropertySource implements ConfigurationPropertySource { public ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name) { if (name null) { return null; } for (PropertyMapper mapper : this.mappers) { try { for (String candidate : mapper.map(name)) { // 这里先通过PropertyMapper转换名称 Object value getPropertySource().getProperty(candidate); // 根据转换后的名称获取获取对应的属性 if (value ! null) { Origin origin PropertySourceOrigin.get(this.propertySource, candidate); return ConfigurationProperty.of(this, name, value, origin); } } } catch (Exception ex) { // Ignore } } return null; } }在创建SpringConfigurationPropertySource对象时会根据PropertySource是MapPropertySource还是SystemEnvironmentPropertySource从而设置不同的mappers属性对于SystemEnvironmentPropertySource它会多一个SystemEnvironmentPropertyMapper。
代码如下class SpringConfigurationPropertySource implements ConfigurationPropertySource { private static final PropertyMapper[] DEFAULT_MAPPERS { DefaultPropertyMapper.INSTANCE }; private static final PropertyMapper[] SYSTEM_ENVIRONMENT_MAPPERS { SystemEnvironmentPropertyMapper.INSTANCE, DefaultPropertyMapper.INSTANCE }; static SpringConfigurationPropertySource from(PropertySource? source) { Assert.notNull(source, Source must not be null); PropertyMapper[] mappers getPropertyMappers(source); if (isFullEnumerable(source)) { return new SpringIterableConfigurationPropertySource((EnumerablePropertySource?) source, mappers); } return new SpringConfigurationPropertySource(source, mappers); } private static PropertyMapper[] getPropertyMappers(PropertySource? source) { // 这里判断了如果是SystemEnvironmentPropertySource则会返回SYSTEM_ENVIRONMENT_MAPPERS里面包含了SystemEnvironmentPropertyMapper if (source instanceof SystemEnvironmentPropertySource hasSystemEnvironmentName(source)) { return SYSTEM_ENVIRONMENT_MAPPERS; } return DEFAULT_MAPPERS; } }对于DefaultPropertyMapper它的map()方法会直接返回ConfigurationPropertyName的名称在本案例中就会直接返回test.container-name。
代码如下final class DefaultPropertyMapper implements PropertyMapper { Override public ListString map(ConfigurationPropertyName configurationPropertyName) { // Use a local copy in case another thread changes things LastMappingConfigurationPropertyName, ListString last this.lastMappedConfigurationPropertyName; if (last ! null last.isFrom(configurationPropertyName)) { return last.getMapping(); } // 这里直接返回ConfigurationPropertyName的名称 String convertedName configurationPropertyName.toString(); ListString mapping Collections.singletonList(convertedName); this.lastMappedConfigurationPropertyName new LastMapping(configurationPropertyName, mapping); return mapping; } }对于SystemEnvironmentPropertyMapper它会返回两个格式的名称在本案例中就会返回TEST_CONTAINERNAME和TEST_CONTAINER_NAME两种格式。
代码如下final class SystemEnvironmentPropertyMapper implements PropertyMapper { public static final PropertyMapper INSTANCE new SystemEnvironmentPropertyMapper(); Override public ListString map(ConfigurationPropertyName configurationPropertyName) { String name convertName(configurationPropertyName); String legacyName convertLegacyName(configurationPropertyName); if (name.equals(legacyName)) { return Collections.singletonList(name); } // 这里会返回两个格式的名称 return Arrays.asList(name, legacyName); } private String convertName(ConfigurationPropertyName name) { return convertName(name, name.getNumberOfElements()); } private String convertName(ConfigurationPropertyName name, int numberOfElements) { StringBuilder result new StringBuilder(); for (int i 0; i numberOfElements; i) { if (!result.isEmpty()) { result.append(_); } result.append(name.getElement(i, Form.UNIFORM).toUpperCase(Locale.ENGLISH)); } return result.toString(); } private String convertLegacyName(ConfigurationPropertyName name) { StringBuilder result new StringBuilder(); for (int i 0; i name.getNumberOfElements(); i) { if (!result.isEmpty()) { result.append(_); } result.append(convertLegacyNameElement(name.getElement(i, Form.ORIGINAL))); } return result.toString(); } private Object convertLegacyNameElement(String element) { return element.replace(-, _).toUpperCase(Locale.ENGLISH); } }在本案例中decryptedSystemEnvironment的PropertySource类型是MapPropertySource存放的内容是TEST_CONTAINER_NAMETomcat。
它只有DefaultPropertyMapper名称为systemEnvironment的PropertySource类型是SystemEnvironmentPropertySource存放的内容是TEST_CONTAINER_NAMEENC_VG9tY2F0。
它有DefaultPropertyMapper和SystemEnvironmentPropertyMapper。
decryptedSystemEnvironment在顺序上排在systemEnvironment前面。
这个时候开始查找传入名称为test.container-name的ConfigurationPropertyName这个时候先从decryptedSystemEnvironment开始找经过DefaultPropertyMapper转换之后拿到的属性名称是test.container-name配置里面没有这个配置然后从systemEnvironment开始找经过SystemEnvironmentPropertyMapper转换之后拿到的属性名称是TEST_CONTAINERNAME和TEST_CONTAINER_NAME根据TEST_CONTAINER_NAME就拿到了ENC_VG9tY2F0。
这就解释了为啥配置类注入的还是加密之后的值了。
问题解决知道问题的原理了之后问题就好解决了。
一种方法是可以在DecryptEnvironmentPostProcessor类的postProcessBeforeInitialization()方法中把添加的MapPropertySource类型改为SystemEnvironmentPropertySource就可以了。
代码如下Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { String systemEnvName StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME; MapPropertySource systemEnvSource (MapPropertySource) environment.getPropertySources().get(systemEnvName); MapString, Object 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(Base