背景
目前項目中使用 jasypt 來做配置項的加解密,但是在實際使用中發(fā)現(xiàn) 3.0.4 版本中 ,在配置中心動態(tài)刷新后,@ConfigurationProperties 的屬性全部變成加密數(shù)據(jù)(如 ENC(XXX=))
接下來是源碼和部分機制原理分析,如果關(guān)心解決方法的話,可以快進到 “解決思路” 章節(jié)
jasypt 原理分析
Spring 框架中,所有的配置數(shù)據(jù)都存儲在 PropertySource 中,我們經(jīng)常在 Spring 框架中使用 Environment 來讀取配置數(shù)據(jù)。Environment 內(nèi)部包含多個 PropertySource(例如 bootstrap.yml 和 application.yml 會被解析為兩個獨立的 PropertySource)。當調(diào)用 Environment.getProperty 時,其內(nèi)部會遍歷所有的 PropertySource,最終通過 PropertySource.getProperty 來尋找指定配置。
所以 jasypt 的核心思路就是代理(或者說包裝)所有的 PropertySource
jasypt 使用 EnableEncryptablePropertiesBeanFactoryPostProcessor 在Bean 初始化之前代理所有 PropertySource。代理后的類我們用 EncryptablePropertySource 來同一稱呼。

EncryptablePropertySource.getProperty 實現(xiàn)邏輯大致如下:當讀取到配置后,如果 value 的格式是 ENC(xxx),將會調(diào)用解密方法對加密數(shù)據(jù)進行解密
用偽代碼和圖總結(jié)下上面的文字


Spring Cloud 動態(tài)刷新配置機制分析
Spring Cloud 為標記 @ConfigurationProperties 的類提供了動態(tài)刷新的功能。文檔:https://cloud.spring.io/spring-cloud-static/spring-cloud.html#_environment_changes
這里我的配置中心是 Nacos(任何配置中心都可以通過發(fā)布事件或者調(diào)用 Spring Cloud endpoint 的方式來刷新配置)
當 Nacos 監(jiān)聽到配置文件更新時,會發(fā)布 RefreshEvent。發(fā)布事件后,代碼會執(zhí)行到 ContextRefresher,該類主要作用是通過創(chuàng)建一個新的 Spring Context 初始化(為了加載新的配置),然后發(fā)送 EnvironmentChangeEvent

當執(zhí)行完 93 行時,最新的配置已經(jīng)加載完畢。其實創(chuàng)建新的 Spring Context 也會執(zhí)行到 EnableEncryptablePropertiesBeanFactoryPostProcessor 邏輯,但是由于加載順序的原因,生成代理類之前并沒有加載 Nacos 的配置。
但是 jasypt 提供了一個 RefreshScopeRefreshedEventListener,該類監(jiān)聽了 RefreshScopeRefreshedEvent, EnvironmentChangeEvent, ServletWebServerInitializedEvent 提供了和 EnableEncryptablePropertiesBeanFactoryPostProcessor 類似的邏輯,將 PropertySource 轉(zhuǎn)換(代理)為 EncryptablePropertySource

問題發(fā)現(xiàn)
但是此時發(fā)現(xiàn) PropertySource 可以被成功代理,但是配置密文卻無法解密,一番 debug 之后發(fā)現(xiàn),執(zhí)行 @ConfigurationProperties 綁定的類 ConfigurationPropertiesRebinder 先于 RefreshScopeRefreshedEventListener 執(zhí)行。
所以現(xiàn)在此時的問題就是如何調(diào)整 RefreshScopeRefreshedEventListener 的執(zhí)行順序。
這時我發(fā)現(xiàn)了 RefreshScopeRefreshedEventListener 使用了 @Order 注解并且實現(xiàn)了 Ordered 接口


但是發(fā)現(xiàn)兩個順序并不一致(應(yīng)該是作者的疏忽),并且在實際代碼運行中,注解并不生效,這里我嘗試將 getOrder 順序修改為與直接一致,然后重新調(diào)試,發(fā)現(xiàn)問題已經(jīng)解決。
解決方案
-
調(diào)整 RefreshScopeRefreshedEventListener 順序,保證該類執(zhí)行順序先于 ConfigurationPropertiesRebinder
可以選擇將順序調(diào)為
HIGHEST_PRECEDENCE,也可以選擇僅優(yōu)先于 RefreshScopeRefreshedEventListener ,不影響其他 Listener -
參考 EnvironmentDecryptApplicationInitializer,使用 ApplicationContextInitializer 實現(xiàn)對 PropertySource 的代理。
在編寫本文時,發(fā)現(xiàn) Spring Cloud 提供了類似 jasypt 的加解密方案,通過 debug 發(fā)現(xiàn) EnvironmentDecryptApplicationInitializer 的執(zhí)行時機要比 jasypt 更合適(不需要像 jasypt 一樣寫兩個類處理),更多細節(jié)讀者自行閱讀 EnvironmentDecryptApplicationInitializer 源碼
@Value 動態(tài)更新
@Value 方式動態(tài)更新并不會受到影響,因為本質(zhì)上 @Value 和 @ConfigurationProperties 的刷新機制不同
更新于 2023 年 2 月 1 日,我的 PR 已經(jīng)被作者 merge,用戶升級到 3.0.5 即可
https://github.com/ulisesbocchio/jasypt-spring-boot/pull/344