jasypt 3.0.4 Bug 分析与解决
背景
目前项目中使用 jasypt 来做配置项的加解密,但是在实际使用中发现 3.0.4 版本中 ,在配置中心动态刷新后,@ConfigurationProperties 的属性全部变成加密数据(如 ENC(XXX=)
)
接下来是源码和部分机制原理分析,如果关心解决方法的话,可以快进到 “解决思路” 章节
jasypt 原理分析
Spring 框架中,所有的配置数据都存储在 PropertySource
中,我们经常在 Spring 框架中使用 Environment
来读取配置数据。Environment
内部包含多个 PropertySource
(例如 bootstrap.yml 和 application.yml 会被解析为两个独立的 PropertySource
)。当调用 Environment.getProperty
时,其内部会遍历所有的 PropertySource
,最终通过 PropertySource.getProperty
来寻找指定配置。
所以 jasypt 的核心思路就是代理(或者说包装)所有的 PropertySource
jasypt 使用 EnableEncryptablePropertiesBeanFactoryPostProcessor
在Bean 初始化之前代理所有 PropertySource
。代理后的类我们用 EncryptablePropertySource
来同一称呼。
EncryptablePropertySource.getProperty
实现逻辑大致如下:当读取到配置后,如果 value 的格式是 ENC(xxx),将会调用解密方法对加密数据进行解密
用伪代码和图总结下上面的文字
小结Spring Cloud 动态刷新配置机制分析
Spring Cloud 为标记 @ConfigurationProperties 的类提供了动态刷新的功能。文档:https://cloud.spring.io/spring-cloud-static/spring-cloud.html#_environment_changes
这里我的配置中心是 Nacos(任何配置中心都可以通过发布事件或者调用 Spring Cloud endpoint 的方式来刷新配置)
当 Nacos 监听到配置文件更新时,会发布 RefreshEvent
。发布事件后,代码会执行到 ContextRefresher,该类主要作用是通过创建一个新的 Spring Context 初始化(为了加载新的配置),然后发送 EnvironmentChangeEvent
当执行完 93 行时,最新的配置已经加载完毕。其实创建新的 Spring Context 也会执行到 EnableEncryptablePropertiesBeanFactoryPostProcessor
逻辑,但是由于加载顺序的原因,生成代理类之前并没有加载 Nacos 的配置。
但是 jasypt 提供了一个 RefreshScopeRefreshedEventListener
,该类监听了 RefreshScopeRefreshedEvent, EnvironmentChangeEvent, ServletWebServerInitializedEvent
提供了和 EnableEncryptablePropertiesBeanFactoryPostProcessor
类似的逻辑,将 PropertySource
转换(代理)为 EncryptablePropertySource
问题发现
但是此时发现 PropertySource 可以被成功代理,但是配置密文却无法解密,一番 debug 之后发现,执行 @ConfigurationProperties 绑定的类 ConfigurationPropertiesRebinder
先于 RefreshScopeRefreshedEventListener
执行。
所以现在此时的问题就是如何调整 RefreshScopeRefreshedEventListener
的执行顺序。
这时我发现了 RefreshScopeRefreshedEventListener 使用了 @Order 注解并且实现了 Ordered 接口
RefreshScopeRefreshedEventListener @Order RefreshScopeRefreshedEventListener getOrder但是发现两个顺序并不一致(应该是作者的疏忽),并且在实际代码运行中,注解并不生效,这里我尝试将 getOrder 顺序修改为与直接一致,然后重新调试,发现问题已经解决。
解决方案
-
调整 RefreshScopeRefreshedEventListener 顺序,保证该类执行顺序先于 ConfigurationPropertiesRebinder
可以选择将顺序调为
HIGHEST_PRECEDENCE
,也可以选择仅优先于 RefreshScopeRefreshedEventListener ,不影响其他 Listener -
参考 EnvironmentDecryptApplicationInitializer,使用 ApplicationContextInitializer 实现对 PropertySource 的代理。
在编写本文时,发现 Spring Cloud 提供了类似 jasypt 的加解密方案,通过 debug 发现 EnvironmentDecryptApplicationInitializer 的执行时机要比 jasypt 更合适(不需要像 jasypt 一样写两个类处理),更多细节读者自行阅读 EnvironmentDecryptApplicationInitializer 源码
@Value 动态更新
@Value 方式动态更新并不会受到影响,因为本质上 @Value 和 @ConfigurationProperties 的刷新机制不同