由于生产环境扫描出有开源组件jar包漏洞,于是进行了springboot升级。
升级后打包部署到生产环境,发现数据库连接报错密码错误。初步定位问题是加载的配置文件是jar包内部而非jar包同级目录的config/application.yml文件。
版本升级
组件 | 升级前版本 | 升级后版本 |
---|---|---|
springcloud | Hoxton.SR9 | 2021.0.3 |
springboot | 2.4.3 | 2.6.8 |
排查过程
- 首先怀疑是生产环境配置出了文件,检查配置文件和服务器环境,没发现有错误。
- 接下来在测试环境将配置文件放在config/application.yml,部署此jar包,试图重现此问题,结果并不存在此问题,这就比较奇怪,令人费解。
- 怀疑是springboot设计导致配置文件加载顺序或者配置项读取方式有变。查询了相关资料,spring配置文件的加载顺序并没有变化。
针对jar包内外来说配置文件加载的先后顺序为:
- jar包外的application.yml
- jar包内的application.yml
- jar包外的application-prod.yml
对于读取顺序来讲为:
- 优先读取jar包本身同级目录下的config/application.yml;
- 再读取jar包本身同级目录下的application.yml;
- classpath根目录下的config目录下的application.yml;
- classpath根目录下的application.yml。
如果同时存在.properties和.yml配置文件,会优先加载yml配置。
- 于是考虑配置项读取方式,询问项目成员得知他的确修改了一处关于数据库密码解密的代码。由于公司使用的是自己的一套算法对数据库密码进行加密,加密后作为配置项,因此需要对配置的密文进行解密。
之前获取配置的方式为:
PropertySource<?> applicationConfig = propertySources.get("applicationConfig");
升级后通过上述代码无法获取到配置,因此改为:
ConfigurableEnvironment environment = ((ApplicationEnvironmentPreparedEvent) event).getEnvironment();
MutablePropertySources propertySources = environment.getPropertySources();
Iterator<PropertySource<?>> iterator = propertySources.iterator();
通过阅读代码(看到其中的两层for循环就有种预感),发现的确是代码层面有问题,代码如下:
ConfigurableEnvironment environment = ((ApplicationEnvironmentPreparedEvent) event).getEnvironment();
MutablePropertySources propertySources = environment.getPropertySources();
for (PropertySource<?> propertySource : propertySources) {
boolean applicationConfig = propertySource.getName().contains("application");
if (!applicationConfig) {
continue;
}
Object source = propertySource.getSource();
Map<String, Object> confMap = (Map) source;
for (Map.Entry<String, Object> entry : confMap.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
...
// 解密为result
System.setProperty(key, result);
}
}
从代码看,getPropertySources拿到了所有配置文件,然后在解密配置项时是先从Map中拿到配置,然后解密后将值重新设置到配置项。由于propertySource先执行了jar包外部的配置,然后执行了jar包里面的配置,同一个key的值会被后执行的一次覆盖掉,而MutablePropertySources继承自迭代器Iterable,按说jar包里的配置每次都会覆盖jar包外的。
解决办法
找到了原因,修改就简单了。将外层for去除掉,通过ConfigurableEnvironment.getProperty()直接获取配置项的值,springboot会确保每次都是从jar包外部config/application.yml文件读取配置,存在多个需解密的配置项就在类中新建数组存放配置名称。
String value = environment.getProperty("key");
ConfigurableEnvironment类
从上述可以看到,问题的关键在于如何去干预配置项的读取和设置。于是我们简单看下ConfigurableEnvironment 类的源码。
public interface ConfigurableEnvironment extends Environment, ConfigurablePropertyResolver {
//设置活动的配置文件
void setActiveProfiles(String... profiles);
// 增加活动的配置文件
void addActiveProfile(String profile);
// 设置默认的配置文件
void setDefaultProfiles(String... profiles);
// 获取PropertySource键值组合的集合
MutablePropertySources getPropertySources();
// 系统环境变量
Map<String, Object> getSystemEnvironment();
// 系统配置
Map<String, Object> getSystemProperties();
// 合并
void merge(ConfigurableEnvironment parent);