關(guān)于springboot配置文件密文解密方式
在使用 springboot 或者 springcloud 開(kāi)發(fā)的時(shí)候,通常為了保證系統(tǒng)的安全性,配置文件中的密碼等銘感信息都會(huì)進(jìn)行加密處理,然后在系統(tǒng)啟動(dòng)的時(shí)候?qū)γ芪倪M(jìn)行解密處理。
一、配置文件密文解密
在使用 springboot 或者 springcloud 的時(shí)候,通常會(huì)在 application.yaml 配置文件中配置數(shù)據(jù)庫(kù)的連接信息。
例如:
mysql: driver: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/test?characterEncoding=utf8 username: root password: 4545222 #一般為了信息安全,密碼都會(huì)配置成密文的,比如:password: PASSWORD[ 加密后的密文 ]
而在實(shí)際的項(xiàng)目中,關(guān)于密碼這一類的銘感信息都是經(jīng)過(guò)加密處理的。
例如:
mysql: driver: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/test?characterEncoding=utf8 username: root # BR23C92223KKDNUIQMPLS0009 為經(jīng)過(guò)加密處理的密碼 password: PASSWORD[BR23C92223KKDNUIQMPLS0009]
經(jīng)過(guò)加密的密文密碼在 springboot 項(xiàng)目啟動(dòng)的時(shí)候會(huì)被解密成明文,而熟悉 springboot 或是 spring 源碼的同學(xué)都知道,不管是 springboot 還是 spring 它們的配置文件在項(xiàng)目啟動(dòng)后都會(huì)被加載到 Environment 對(duì)象中,而在 springboot 中,在系統(tǒng)的 Environment 對(duì)象創(chuàng)建完成并初始化好了之后,會(huì)發(fā)布一個(gè)事件:ApplicationEnvironmentPreparedEvent 。
清楚了以上這兩點(diǎn),那么我們實(shí)現(xiàn)配置文件密文解密成對(duì)應(yīng)的明文也就有了思路,我們只需要定義一個(gè)監(jiān)聽(tīng)器監(jiān)聽(tīng) ApplicationEnvironmentPreparedEvent 事件,當(dāng)系統(tǒng)的 Environment 對(duì)象創(chuàng)建和初始化完成后,會(huì)發(fā)布這個(gè)事件,然后我們的監(jiān)聽(tīng)器就能監(jiān)聽(tīng)到這個(gè)事件,最后我們?cè)诒O(jiān)聽(tīng)器中找出所有經(jīng)過(guò)加密的配置項(xiàng),然后進(jìn)行解密,最終再把解密后的明文放入 Environment 對(duì)象中。這樣我們就實(shí)現(xiàn)了對(duì)配置文件中經(jīng)過(guò)加密的配置項(xiàng)解密的功能。
代碼如下:
package cn.yjh.listener; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent; import org.springframework.boot.env.OriginTrackedMapPropertySource; import org.springframework.context.ApplicationListener; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; import cn.yjh.util.EncryptUtil; /** * @author YouJinhua * @since 2021/9/13 10:21 */ public class EnvironmentPreparedListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> { @Override public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) { ConfigurableEnvironment env = event.getEnvironment(); MutablePropertySources pss = env.getPropertySources(); List<PropertySource> list = new ArrayList<>(); for(PropertySource ps : pss){ Map<String,Object> map = new HashMap<>(); if(ps instanceof OriginTrackedMapPropertySource){ OriginTrackedMapPropertySource propertySource = new OriginTrackedMapPropertySource(ps.getName(),map); Map<String,Object> src = (Map<String,Object>)ps.getSource(); src.forEach((k,v)->{ String strValue = String.valueOf(v); if(strValue.startsWith("PASSWORD[") && strValue.endsWith("]")) { // 此處進(jìn)行截取出對(duì)應(yīng)的密文 BR23C92223KKDNUIQMPLS0009 ,然后調(diào)用對(duì)應(yīng)的解密算法進(jìn)行解密操作 v = EncryptUtil.decrypt("work0", strValue.substring(9, strValue.length()-1)); } map.put(k,v); }); list.add(propertySource); } } /** 此處是刪除原來(lái)的 OriginTrackedMapPropertySource 對(duì)象, 把解密后新生成的放入到 Environment,為什么不直接修改原來(lái)的 OriginTrackedMapPropertySource 對(duì)象,此處不做過(guò)多解釋 不懂的可以去看看它對(duì)應(yīng)的源碼,也算是留一個(gè)懸念,也是希望大家 能夠沒(méi)事多看一看源碼。 */ list.forEach(ps->{ pss.remove(ps.getName()); pss.addLast(ps); }); } }
接下來(lái)就是如何讓我們的監(jiān)聽(tīng)器生效了,了解 springboot 自動(dòng)裝配原理的同學(xué),大家都知道接下來(lái)要做什么了,首先在我們的 resources 目錄下新建一個(gè) META-INF 目錄,然后在這個(gè)目錄下新建 spring.factories 文件,在文件中加這么一句話:
org.springframework.context.ApplicationListener=cn.yjh.listener.EnvironmentPreparedListener
代碼如下:
# Application Listeners org.springframework.context.ApplicationListener=cn.yjh.listener.EnvironmentPreparedListener
這樣我們的配置文件密文解密功能就實(shí)現(xiàn)了。
二、配置中心密文解密( 以springcloud+nacos為例 )
springcloud + nacos 配置中心的環(huán)境搭建,這里就不做過(guò)多的說(shuō)明了,還不會(huì)的小伙伴,可以看看其他的博客
其實(shí)不光是我們的配置文件需要加密,從配置中心拉取的配置也是需要加密的。那么從配置中心拉取下來(lái)的配置項(xiàng)我們?nèi)绾芜M(jìn)行解密呢?其實(shí)具體的實(shí)現(xiàn)思路和配置文件的方式差不多。網(wǎng)上也有對(duì)應(yīng)成熟的開(kāi)源 jar 包(jasypt-spring-boot-starter)可以實(shí)現(xiàn)這個(gè)功能,這里我不講那種實(shí)現(xiàn)方式了,盡管哪種方式使用起來(lái)也挺簡(jiǎn)單方便的,不會(huì)的小伙伴可以看看其他博客或者官方文檔。
我這里講的實(shí)現(xiàn)方式是不需要導(dǎo)入任何的jar包的,因?yàn)閟pringcloud自己本身都有這方面的實(shí)現(xiàn),只是很少人知道,官方文檔講得也比較的難懂。其實(shí)當(dāng)你搭建完springcloud的項(xiàng)目后,你去查看它的jar包依賴,你會(huì)發(fā)現(xiàn)默認(rèn)已經(jīng)導(dǎo)入了一個(gè)jar包:
這是一個(gè)接口,是我們實(shí)現(xiàn)解密的關(guān)鍵點(diǎn),因?yàn)楫?dāng)我們的 Environment 對(duì)象的數(shù)據(jù)發(fā)生變化時(shí)候都會(huì)通過(guò)事件回調(diào)的機(jī)制去調(diào)用這個(gè)接口的實(shí)現(xiàn)類的decrypt()解密方法,我們先來(lái)看一段springcloud的源碼,再來(lái)分析我們的實(shí)現(xiàn)思路,先看:EncryptionBootstrapConfiguration 的關(guān)鍵源碼:
// 這個(gè)注解說(shuō)明是一個(gè)配置類 @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ TextEncryptor.class }) @EnableConfigurationProperties({ KeyProperties.class }) public class EncryptionBootstrapConfiguration { @Autowired(required = false) // 這個(gè)地方會(huì)從IOC容器中獲取上面我們提到的那個(gè)接口的實(shí)現(xiàn)類,由于是required = false,所以不一定獲取得到,因?yàn)榭赡苋萜髦袥](méi)有這個(gè)對(duì)象 private TextEncryptor encryptor; @Autowired private KeyProperties key; // 這里 spring IOC 容器添加一個(gè) EnvironmentDecryptApplicationInitializer 組件 @Bean public EnvironmentDecryptApplicationInitializer environmentDecryptApplicationListener() { // 這里判斷上面注入的 TextEncryptor 對(duì)象是否為空 if (this.encryptor == null) { //為null,就創(chuàng)建一個(gè)默認(rèn)的 this.encryptor = new FailsafeTextEncryptor(); } // 否則使用上面注入的那個(gè) TextEncryptor EnvironmentDecryptApplicationInitializer listener = new EnvironmentDecryptApplicationInitializer( this.encryptor); listener.setFailOnError(this.key.isFailOnError()); return listener; } /** 省略其他代碼,只看關(guān)鍵的 */ }
再看這個(gè) EnvironmentDecryptApplicationInitializer 類的源碼:
public class EnvironmentDecryptApplicationInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered { /** 這里的 {cipher} 相當(dāng)于我們 springboot配置文件解密 的 PASSWORD[] springcloud的配置格式是: '{cipher}BR23C92223KKDNUIQMPLS0009' 而我們的配置格式是: PASSWORD[BR23C92223KKDNUIQMPLS0009] 注意: '' 必須要加,不然yaml解析器,解析不了,會(huì)報(bào)錯(cuò)。 */ public static final String ENCRYPTED_PROPERTY_PREFIX = "{cipher}"; // 解密的對(duì)象 private TextEncryptor encryptor; // 構(gòu)造函數(shù),傳入解密對(duì)象,前一個(gè)配置類傳入的 public EnvironmentDecryptApplicationInitializer(TextEncryptor encryptor) { // 進(jìn)行屬性賦值 this.encryptor = encryptor; } // 這個(gè)方法,我們看關(guān)鍵點(diǎn) private void merge(PropertySource<?> source, Map<String, Object> properties) { if (source instanceof CompositePropertySource) { List<PropertySource<?>> sources = new ArrayList<>( ((CompositePropertySource) source).getPropertySources()); Collections.reverse(sources); for (PropertySource<?> nested : sources) { merge(nested, properties); } } else if (source instanceof EnumerablePropertySource) { Map<String, Object> otherCollectionProperties = new LinkedHashMap<>(); boolean sourceHasDecryptedCollection = false; EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) source; for (String key : enumerable.getPropertyNames()) { Object property = source.getProperty(key); if (property != null) { String value = property.toString(); // 這里決定了我們,要使用 {cipher} 開(kāi)頭,表面我們是一個(gè)加密項(xiàng) if (value.startsWith(ENCRYPTED_PROPERTY_PREFIX)) { // 如何是加密項(xiàng),放入properties對(duì)象中存起來(lái),方便后面解密 properties.put(key, value); if (COLLECTION_PROPERTY.matcher(key).matches()) { sourceHasDecryptedCollection = true; } } else if (COLLECTION_PROPERTY.matcher(key).matches()) { // put non-encrypted properties so merging of index properties // happens correctly otherCollectionProperties.put(key, value); } else { // override previously encrypted with non-encrypted property properties.remove(key); } } } // copy all indexed properties even if not encrypted if (sourceHasDecryptedCollection && !otherCollectionProperties.isEmpty()) { properties.putAll(otherCollectionProperties); } } } private void decrypt(Map<String, Object> properties) { properties.replaceAll((key, value) -> { String valueString = value.toString(); if (!valueString.startsWith(ENCRYPTED_PROPERTY_PREFIX)) { return value; } return decrypt(key, valueString); }); } // 這里是真正調(diào)用解密方法進(jìn)行解密了 private String decrypt(String key, String original) { String value = original.substring(ENCRYPTED_PROPERTY_PREFIX.length()); try { // 這里的 encryptor 對(duì)象就是構(gòu)造函數(shù)傳入的 TextEncryptor value = this.encryptor.decrypt(value); if (logger.isDebugEnabled()) { logger.debug("Decrypted: key=" + key); } return value; } catch (Exception e) { String message = "Cannot decrypt: key=" + key; if (logger.isDebugEnabled()) { logger.warn(message, e); } else { logger.warn(message); } if (this.failOnError) { throw new IllegalStateException(message, e); } return ""; } } }
以上兩個(gè)類的源碼,我這里省略了很多,想仔細(xì)查看的自己可以去看看這兩個(gè)類,我這里關(guān)鍵的地方都已經(jīng)做了注釋。
這里給大家梳理一下流程:
- @Configuration標(biāo)注EncryptionBootstrapConfiguration 類,說(shuō)明是個(gè)配置類
- 既然是配置類那么必然是要導(dǎo)入組件到spring中
- @Autowired 注入TextEncryptor ,默認(rèn)IOC容器中是沒(méi)有的這個(gè)對(duì)象的,所以注入失敗,值為null
- TextEncryptor 值為null,就會(huì)創(chuàng)建一個(gè)默認(rèn)的 this.encryptor = new FailsafeTextEncryptor();
- @Bean 導(dǎo)入EnvironmentDecryptApplicationInitializer 這個(gè)組件,構(gòu)造函數(shù)傳入 TextEncryptor
- 接下來(lái)就是找到對(duì)應(yīng)的加密配置項(xiàng) if (value.startsWith(ENCRYPTED_PROPERTY_PREFIX))
- 然后調(diào)用 TextEncryptor接口實(shí)現(xiàn)對(duì)象的decrypt()方法執(zhí)行解密操作。
通過(guò)上面的分析我們知道解密的關(guān)鍵點(diǎn)就是TextEncryptor,如果我們?cè)诩虞dEncryptionBootstrapConfiguration 配置類之前,給IOC容器中加入一個(gè)我們自己實(shí)現(xiàn)的解密算法,那么等到注入TextEncryptor 的時(shí)候,就不會(huì)為空了,也就不會(huì)創(chuàng)建默認(rèn)的FailsafeTextEncryptor對(duì)象,那么在解密的時(shí)候不就執(zhí)行我們自己的解密算法了嗎?
現(xiàn)在的問(wèn)題就是要解決:
在何時(shí)加入,如何加入這個(gè)自己實(shí)現(xiàn)的解密算法到IOC容器中,這個(gè)時(shí)候又想到了spring、springboot、springcloud的各種擴(kuò)展點(diǎn)了,熟悉這些擴(kuò)展點(diǎn)的都知道
ApplicationPreparedEvent 事件,在 BeanFactory 創(chuàng)建完成后,但是還并沒(méi)有執(zhí)行refresh()方法的時(shí)候,就會(huì)發(fā)布這個(gè)事件,因?yàn)槲覀冎澜馕雠渲妙愂菍儆趓efresh()中的一步,所以這樣的思路是可行的。
實(shí)現(xiàn)代碼如下:
package cn.yjh.listener; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.boot.context.event.ApplicationPreparedEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.core.Ordered; import org.springframework.security.crypto.encrypt.TextEncryptor; /** * @author YouJinhua * @since 2021/9/13 9:10 */ public class RegisterTextEncryptorListener implements ApplicationListener<ApplicationPreparedEvent>, Ordered { @Override public void onApplicationEvent(ApplicationPreparedEvent event) { ConfigurableApplicationContext applicationContext = event.getApplicationContext(); // 這里回往spring IOC 中添加好幾次,是因?yàn)楦缸尤萜鞯脑?,所以要判斷一? if(applicationContext instanceof AnnotationConfigApplicationContext){ ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory(); // 這里判斷是否已經(jīng)添加過(guò)我們自己的解密算法了,沒(méi)添加才添加,否則跳過(guò) if(!beanFactory.containsBean("textEncryptor")){ beanFactory.registerSingleton("textEncryptor",new TextEncryptor(){ @Override public String encrypt(String text) { System.out.println("=====================================加密"); return "加密"+text; } @Override public String decrypt(String encryptedText) { //這里解密就直接輸出日志,然后直接解密返回 System.out.println("=====================================解密"); return EncryptUtil.decrypt("work0", encryptedText); } }); } } } @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } }
接下來(lái),就是讓我們的監(jiān)聽(tīng)器生效了,老規(guī)矩,在spring.factories中加上這么一句話:
org.springframework.context.ApplicationListener=cn.yjh.listener.RegisterTextEncryptorListener
這樣就可以了,注意配置中心配置加密項(xiàng)的時(shí)候一定要注意格式,否則解析不了會(huì)報(bào)錯(cuò),正確格式如下:
mysql: driver: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/test?characterEncoding=utf8 username: root # BR23C92223KKDNUIQMPLS0009 為經(jīng)過(guò)加密處理的密碼,注意一定要加 '' 否則解析yaml會(huì)報(bào)錯(cuò) password: '{cipher}BR23C92223KKDNUIQMPLS0009'
總結(jié)
springcloud配置中心解密配置項(xiàng),也是看源碼的時(shí)候才發(fā)現(xiàn)原來(lái)springcloud已經(jīng)支持了這個(gè)功能,以前沒(méi)看過(guò)這一塊兒的源碼的時(shí)候,都不知道可以這么實(shí)現(xiàn),以前都是使用:jasypt-spring-boot-starter來(lái)實(shí)現(xiàn)的,所以說(shuō)多看源碼還是會(huì)有所收獲的,這篇文章就到這里。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
springboot多模塊多環(huán)境配置文件問(wèn)題(動(dòng)態(tài)配置生產(chǎn)和開(kāi)發(fā)環(huán)境)
這篇文章主要介紹了springboot多模塊多環(huán)境配置文件問(wèn)題(動(dòng)態(tài)配置生產(chǎn)和開(kāi)發(fā)環(huán)境),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04mybatis-plus阻止全表更新與刪除的實(shí)現(xiàn)
BlockAttackInnerInterceptor 是mybatis-plus的一個(gè)內(nèi)置攔截器,用于防止惡意的全表更新或刪除操作,本文主要介紹了mybatis-plus阻止全表更新與刪除的實(shí)現(xiàn),感興趣的可以了解一下2023-12-12Java線程創(chuàng)建靜態(tài)代理模式代碼實(shí)例
這篇文章主要介紹了Java線程創(chuàng)建靜態(tài)代理模式代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-11-11IntelliJ IDEA 如何徹底刪除項(xiàng)目的步驟
本篇文章主要介紹了IntelliJ IDEA 如何徹底刪除項(xiàng)目的步驟,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-11-11Spring實(shí)現(xiàn)Quartz自動(dòng)配置的方法詳解
這篇文章主要介紹了Spring實(shí)現(xiàn)Quartz自動(dòng)配置的方法詳解,如果想在應(yīng)用中使用Quartz任務(wù)調(diào)度功能,可以通過(guò)Spring Boot實(shí)現(xiàn)Quartz的自動(dòng)配置,以下介紹如何開(kāi)啟Quartz自動(dòng)配置,以及Quartz自動(dòng)配置的實(shí)現(xiàn)過(guò)程,需要的朋友可以參考下2023-11-11Java實(shí)現(xiàn)簡(jiǎn)易購(gòu)物系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)簡(jiǎn)易購(gòu)物系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05深入理解java動(dòng)態(tài)代理的兩種實(shí)現(xiàn)方式(JDK/Cglib)
本篇文章主要介紹了java動(dòng)態(tài)代理的兩種實(shí)現(xiàn)方式,詳細(xì)的介紹了JDK和Cglib的實(shí)現(xiàn)方法,具有一定的參考價(jià)值,有興趣的可以了解一下2017-04-04