關(guān)于springboot配置文件密文解密方式
在使用 springboot 或者 springcloud 開發(fā)的時候,通常為了保證系統(tǒng)的安全性,配置文件中的密碼等銘感信息都會進行加密處理,然后在系統(tǒng)啟動的時候?qū)γ芪倪M行解密處理。
一、配置文件密文解密
在使用 springboot 或者 springcloud 的時候,通常會在 application.yaml 配置文件中配置數(shù)據(jù)庫的連接信息。
例如:
mysql: driver: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/test?characterEncoding=utf8 username: root password: 4545222 #一般為了信息安全,密碼都會配置成密文的,比如:password: PASSWORD[ 加密后的密文 ]
而在實際的項目中,關(guān)于密碼這一類的銘感信息都是經(jīng)過加密處理的。
例如:
mysql: driver: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/test?characterEncoding=utf8 username: root # BR23C92223KKDNUIQMPLS0009 為經(jīng)過加密處理的密碼 password: PASSWORD[BR23C92223KKDNUIQMPLS0009]
經(jīng)過加密的密文密碼在 springboot 項目啟動的時候會被解密成明文,而熟悉 springboot 或是 spring 源碼的同學都知道,不管是 springboot 還是 spring 它們的配置文件在項目啟動后都會被加載到 Environment 對象中,而在 springboot 中,在系統(tǒng)的 Environment 對象創(chuàng)建完成并初始化好了之后,會發(fā)布一個事件:ApplicationEnvironmentPreparedEvent 。
清楚了以上這兩點,那么我們實現(xiàn)配置文件密文解密成對應(yīng)的明文也就有了思路,我們只需要定義一個監(jiān)聽器監(jiān)聽 ApplicationEnvironmentPreparedEvent 事件,當系統(tǒng)的 Environment 對象創(chuàng)建和初始化完成后,會發(fā)布這個事件,然后我們的監(jiān)聽器就能監(jiān)聽到這個事件,最后我們在監(jiān)聽器中找出所有經(jīng)過加密的配置項,然后進行解密,最終再把解密后的明文放入 Environment 對象中。這樣我們就實現(xiàn)了對配置文件中經(jī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("]")) { // 此處進行截取出對應(yīng)的密文 BR23C92223KKDNUIQMPLS0009 ,然后調(diào)用對應(yīng)的解密算法進行解密操作 v = EncryptUtil.decrypt("work0", strValue.substring(9, strValue.length()-1)); } map.put(k,v); }); list.add(propertySource); } } /** 此處是刪除原來的 OriginTrackedMapPropertySource 對象, 把解密后新生成的放入到 Environment,為什么不直接修改原來的 OriginTrackedMapPropertySource 對象,此處不做過多解釋 不懂的可以去看看它對應(yīng)的源碼,也算是留一個懸念,也是希望大家 能夠沒事多看一看源碼。 */ list.forEach(ps->{ pss.remove(ps.getName()); pss.addLast(ps); }); } }
接下來就是如何讓我們的監(jiān)聽器生效了,了解 springboot 自動裝配原理的同學,大家都知道接下來要做什么了,首先在我們的 resources 目錄下新建一個 META-INF 目錄,然后在這個目錄下新建 spring.factories 文件,在文件中加這么一句話:
org.springframework.context.ApplicationListener=cn.yjh.listener.EnvironmentPreparedListener
代碼如下:
# Application Listeners org.springframework.context.ApplicationListener=cn.yjh.listener.EnvironmentPreparedListener
這樣我們的配置文件密文解密功能就實現(xiàn)了。
二、配置中心密文解密( 以springcloud+nacos為例 )
springcloud + nacos 配置中心的環(huán)境搭建,這里就不做過多的說明了,還不會的小伙伴,可以看看其他的博客
其實不光是我們的配置文件需要加密,從配置中心拉取的配置也是需要加密的。那么從配置中心拉取下來的配置項我們?nèi)绾芜M行解密呢?其實具體的實現(xiàn)思路和配置文件的方式差不多。網(wǎng)上也有對應(yīng)成熟的開源 jar 包(jasypt-spring-boot-starter)可以實現(xiàn)這個功能,這里我不講那種實現(xiàn)方式了,盡管哪種方式使用起來也挺簡單方便的,不會的小伙伴可以看看其他博客或者官方文檔。
我這里講的實現(xiàn)方式是不需要導(dǎo)入任何的jar包的,因為springcloud自己本身都有這方面的實現(xiàn),只是很少人知道,官方文檔講得也比較的難懂。其實當你搭建完springcloud的項目后,你去查看它的jar包依賴,你會發(fā)現(xiàn)默認已經(jīng)導(dǎo)入了一個jar包:
這是一個接口,是我們實現(xiàn)解密的關(guān)鍵點,因為當我們的 Environment 對象的數(shù)據(jù)發(fā)生變化時候都會通過事件回調(diào)的機制去調(diào)用這個接口的實現(xiàn)類的decrypt()解密方法,我們先來看一段springcloud的源碼,再來分析我們的實現(xiàn)思路,先看:EncryptionBootstrapConfiguration 的關(guān)鍵源碼:
// 這個注解說明是一個配置類 @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ TextEncryptor.class }) @EnableConfigurationProperties({ KeyProperties.class }) public class EncryptionBootstrapConfiguration { @Autowired(required = false) // 這個地方會從IOC容器中獲取上面我們提到的那個接口的實現(xiàn)類,由于是required = false,所以不一定獲取得到,因為可能容器中沒有這個對象 private TextEncryptor encryptor; @Autowired private KeyProperties key; // 這里 spring IOC 容器添加一個 EnvironmentDecryptApplicationInitializer 組件 @Bean public EnvironmentDecryptApplicationInitializer environmentDecryptApplicationListener() { // 這里判斷上面注入的 TextEncryptor 對象是否為空 if (this.encryptor == null) { //為null,就創(chuàng)建一個默認的 this.encryptor = new FailsafeTextEncryptor(); } // 否則使用上面注入的那個 TextEncryptor EnvironmentDecryptApplicationInitializer listener = new EnvironmentDecryptApplicationInitializer( this.encryptor); listener.setFailOnError(this.key.isFailOnError()); return listener; } /** 省略其他代碼,只看關(guān)鍵的 */ }
再看這個 EnvironmentDecryptApplicationInitializer 類的源碼:
public class EnvironmentDecryptApplicationInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered { /** 這里的 {cipher} 相當于我們 springboot配置文件解密 的 PASSWORD[] springcloud的配置格式是: '{cipher}BR23C92223KKDNUIQMPLS0009' 而我們的配置格式是: PASSWORD[BR23C92223KKDNUIQMPLS0009] 注意: '' 必須要加,不然yaml解析器,解析不了,會報錯。 */ public static final String ENCRYPTED_PROPERTY_PREFIX = "{cipher}"; // 解密的對象 private TextEncryptor encryptor; // 構(gòu)造函數(shù),傳入解密對象,前一個配置類傳入的 public EnvironmentDecryptApplicationInitializer(TextEncryptor encryptor) { // 進行屬性賦值 this.encryptor = encryptor; } // 這個方法,我們看關(guā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} 開頭,表面我們是一個加密項 if (value.startsWith(ENCRYPTED_PROPERTY_PREFIX)) { // 如何是加密項,放入properties對象中存起來,方便后面解密 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)用解密方法進行解密了 private String decrypt(String key, String original) { String value = original.substring(ENCRYPTED_PROPERTY_PREFIX.length()); try { // 這里的 encryptor 對象就是構(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 ""; } } }
以上兩個類的源碼,我這里省略了很多,想仔細查看的自己可以去看看這兩個類,我這里關(guān)鍵的地方都已經(jīng)做了注釋。
這里給大家梳理一下流程:
- @Configuration標注EncryptionBootstrapConfiguration 類,說明是個配置類
- 既然是配置類那么必然是要導(dǎo)入組件到spring中
- @Autowired 注入TextEncryptor ,默認IOC容器中是沒有的這個對象的,所以注入失敗,值為null
- TextEncryptor 值為null,就會創(chuàng)建一個默認的 this.encryptor = new FailsafeTextEncryptor();
- @Bean 導(dǎo)入EnvironmentDecryptApplicationInitializer 這個組件,構(gòu)造函數(shù)傳入 TextEncryptor
- 接下來就是找到對應(yīng)的加密配置項 if (value.startsWith(ENCRYPTED_PROPERTY_PREFIX))
- 然后調(diào)用 TextEncryptor接口實現(xiàn)對象的decrypt()方法執(zhí)行解密操作。
通過上面的分析我們知道解密的關(guān)鍵點就是TextEncryptor,如果我們在加載EncryptionBootstrapConfiguration 配置類之前,給IOC容器中加入一個我們自己實現(xiàn)的解密算法,那么等到注入TextEncryptor 的時候,就不會為空了,也就不會創(chuàng)建默認的FailsafeTextEncryptor對象,那么在解密的時候不就執(zhí)行我們自己的解密算法了嗎?
現(xiàn)在的問題就是要解決:
在何時加入,如何加入這個自己實現(xiàn)的解密算法到IOC容器中,這個時候又想到了spring、springboot、springcloud的各種擴展點了,熟悉這些擴展點的都知道
ApplicationPreparedEvent 事件,在 BeanFactory 創(chuàng)建完成后,但是還并沒有執(zhí)行refresh()方法的時候,就會發(fā)布這個事件,因為我們知道解析配置類是屬于refresh()中的一步,所以這樣的思路是可行的。
實現(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 中添加好幾次,是因為父子容器的原因,所以要判斷一下 if(applicationContext instanceof AnnotationConfigApplicationContext){ ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory(); // 這里判斷是否已經(jīng)添加過我們自己的解密算法了,沒添加才添加,否則跳過 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; } }
接下來,就是讓我們的監(jiān)聽器生效了,老規(guī)矩,在spring.factories中加上這么一句話:
org.springframework.context.ApplicationListener=cn.yjh.listener.RegisterTextEncryptorListener
這樣就可以了,注意配置中心配置加密項的時候一定要注意格式,否則解析不了會報錯,正確格式如下:
mysql: driver: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/test?characterEncoding=utf8 username: root # BR23C92223KKDNUIQMPLS0009 為經(jīng)過加密處理的密碼,注意一定要加 '' 否則解析yaml會報錯 password: '{cipher}BR23C92223KKDNUIQMPLS0009'
總結(jié)
springcloud配置中心解密配置項,也是看源碼的時候才發(fā)現(xiàn)原來springcloud已經(jīng)支持了這個功能,以前沒看過這一塊兒的源碼的時候,都不知道可以這么實現(xiàn),以前都是使用:jasypt-spring-boot-starter來實現(xiàn)的,所以說多看源碼還是會有所收獲的,這篇文章就到這里。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
springboot多模塊多環(huán)境配置文件問題(動態(tài)配置生產(chǎn)和開發(fā)環(huán)境)
這篇文章主要介紹了springboot多模塊多環(huán)境配置文件問題(動態(tài)配置生產(chǎn)和開發(fā)環(huán)境),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-04-04mybatis-plus阻止全表更新與刪除的實現(xiàn)
BlockAttackInnerInterceptor 是mybatis-plus的一個內(nèi)置攔截器,用于防止惡意的全表更新或刪除操作,本文主要介紹了mybatis-plus阻止全表更新與刪除的實現(xiàn),感興趣的可以了解一下2023-12-12Java線程創(chuàng)建靜態(tài)代理模式代碼實例
這篇文章主要介紹了Java線程創(chuàng)建靜態(tài)代理模式代碼實例,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-11-11深入理解java動態(tài)代理的兩種實現(xiàn)方式(JDK/Cglib)
本篇文章主要介紹了java動態(tài)代理的兩種實現(xiàn)方式,詳細的介紹了JDK和Cglib的實現(xiàn)方法,具有一定的參考價值,有興趣的可以了解一下2017-04-04