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 来同一称呼。

EnableEncryptablePropertiesBeanFactoryPostProcessor

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

ContextRefresher

当执行完 93 行时,最新的配置已经加载完毕。其实创建新的 Spring Context 也会执行到 EnableEncryptablePropertiesBeanFactoryPostProcessor 逻辑,但是由于加载顺序的原因,生成代理类之前并没有加载 Nacos 的配置。

但是 jasypt 提供了一个 RefreshScopeRefreshedEventListener,该类监听了 RefreshScopeRefreshedEvent, EnvironmentChangeEvent, ServletWebServerInitializedEvent 提供了和 EnableEncryptablePropertiesBeanFactoryPostProcessor 类似的逻辑,将 PropertySource 转换(代理)为 EncryptablePropertySource

RefreshScopeRefreshedEventListener

问题发现

但是此时发现 PropertySource 可以被成功代理,但是配置密文却无法解密,一番 debug 之后发现,执行 @ConfigurationProperties 绑定的类 ConfigurationPropertiesRebinder 先于 RefreshScopeRefreshedEventListener 执行。

所以现在此时的问题就是如何调整 RefreshScopeRefreshedEventListener 的执行顺序。

这时我发现了 RefreshScopeRefreshedEventListener 使用了 @Order 注解并且实现了 Ordered 接口

RefreshScopeRefreshedEventListener @Order

RefreshScopeRefreshedEventListener getOrder

但是发现两个顺序并不一致(应该是作者的疏忽),并且在实际代码运行中,注解并不生效,这里我尝试将 getOrder 顺序修改为与直接一致,然后重新调试,发现问题已经解决。

解决方案

  1. 调整 RefreshScopeRefreshedEventListener 顺序,保证该类执行顺序先于 ConfigurationPropertiesRebinder

    可以选择将顺序调为 HIGHEST_PRECEDENCE,也可以选择仅优先于 RefreshScopeRefreshedEventListener ,不影响其他 Listener

  2. 参考 EnvironmentDecryptApplicationInitializer,使用 ApplicationContextInitializer 实现对 PropertySource 的代理。

    在编写本文时,发现 Spring Cloud 提供了类似 jasypt 的加解密方案,通过 debug 发现 EnvironmentDecryptApplicationInitializer 的执行时机要比 jasypt 更合适(不需要像 jasypt 一样写两个类处理),更多细节读者自行阅读 EnvironmentDecryptApplicationInitializer 源码

@Value 动态更新

@Value 方式动态更新并不会受到影响,因为本质上 @Value 和 @ConfigurationProperties 的刷新机制不同


更新于 2023 年 2 月 1 日,我的 PR 已经被作者 merge,用户升级到 3.0.5 即可
https://github.com/ulisesbocchio/jasypt-spring-boot/pull/344