springboot 和 springcloud 配置文件和配置中心密文解密实现原理

  • 前言
  • 一、配置文件密文解密
  • 二、配置中心密文解密( 以 springcloud + nacos 为例 )
  • 总结

前言

    在使用 springboot 或者 springcloud 开发的时候,通常为了保证系统的安全性,配置文件中的密码等铭感信息都会进行加密处理,然后在系统启动的时候对密文进行解密处理。


提示:本篇文章属于原创,请勿抄袭。

一、配置文件密文解密

    在使用 springboot 或者 springcloud 的时候,通常会在 application.yaml 配置文件中配置数据库的连接信息。例如:

mysql:driver: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/test?characterEncoding=utf8username: rootpassword: 4545222   #一般为了信息安全,密码都会配置成密文的,比如:password: PASSWORD[ 加密后的密文 ]

    而在实际的项目中,关于密码这一类的铭感信息都是经过加密处理的。例如:

mysql:driver: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/test?characterEncoding=utf8username: root# BR23C92223KKDNUIQMPLS0009 为经过加密处理的密码password: PASSWORD[BR23C92223KKDNUIQMPLS0009]

    经过加密的密文密码在 springboot 项目启动的时候会被解密成明文,而熟悉 springboot 或是 spring 源码的同学都知道,不管是 springboot 还是 spring 它们的配置文件在项目启动后都会被加载到 Environment 对象中,而在 springboot 中,在系统的 Environment 对象创建完成并初始化好了之后,会发布一个事件:ApplicationEnvironmentPreparedEvent 。

    清楚了以上这两点,那么我们实现配置文件密文解密成对应的明文也就有了思路,我们只需要定义一个监听器监听 ApplicationEnvironmentPreparedEvent 事件,当系统的 Environment 对象创建和初始化完成后,会发布这个事件,然后我们的监听器就能监听到这个事件,最后我们在监听器中找出所有经过加密的配置项,然后进行解密,最终再把解密后的明文放入 Environment 对象中。这样我们就实现了对配置文件中经过加密的配置项解密的功能。

代码如下:

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> {@Overridepublic 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("]")) {// 此处进行截取出对应的密文 BR23C92223KKDNUIQMPLS0009 ,然后调用对应的解密算法进行解密操作v = EncryptUtil.decrypt("work0", strValue.substring(9, strValue.length()-1));}map.put(k,v);});list.add(propertySource);}}/** 此处是删除原来的 OriginTrackedMapPropertySource 对象,把解密后新生成的放入到 Environment,为什么不直接修改原来的OriginTrackedMapPropertySource 对象,此处不做过多解释不懂的可以去看看它对应的源码,也算是留一个悬念,也是希望大家能够没事多看一看源码。*/list.forEach(ps->{pss.remove(ps.getName());pss.addLast(ps);});}
}

    接下来就是如何让我们的监听器生效了,了解 springboot 自动装配原理的同学,大家都知道接下来要做什么了,首先在我们的 resources 目录下新建一个 META-INF 目录,然后在这个目录下新建 spring.factories 文件,在文件中加这么一句话:
org.springframework.context.ApplicationListener=cn.yjh.listener.EnvironmentPreparedListener

代码如下:

# Application Listeners
org.springframework.context.ApplicationListener=cn.yjh.listener.EnvironmentPreparedListener

    这样我们的配置文件密文解密功能就实现了。

二、配置中心密文解密( 以 springcloud + nacos 为例 )

    springcloud + nacos 配置中心的环境搭建,这里就不做过多的说明了,还不会的小伙伴,可以看看其他的博客

    其实不光是我们的配置文件需要加密,从配置中心拉取的配置也是需要加密的。那么从配置中心拉取下来的配置项我们如何进行解密呢?其实具体的实现思路和配置文件的方式差不多。网上也有对应成熟的开源 jar 包(jasypt-spring-boot-starter)可以实现这个功能,这里我不讲那种实现方式了,尽管哪种方式使用起来也挺简单方便的,不会的小伙伴可以看看其他博客或者官方文档。

    我这里讲的实现方式是不需要导入任何的jar包的,因为springcloud自己本身都有这方面的实现,只是很少人知道,官方文档讲得也比较的难懂。其实当你搭建完springcloud的项目后,你去查看它的jar包依赖,你会发现默认已经导入了一个jar包:

    这是一个接口,是我们实现解密的关键点,因为当我们的 Environment 对象的数据发生变化时候都会通过事件回调的机制去调用这个接口的实现类的decrypt()解密方法,我们先来看一段springcloud的源码,再来分析我们的实现思路,先看:EncryptionBootstrapConfiguration 的关键源码:

// 这个注解说明是一个配置类
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ TextEncryptor.class })
@EnableConfigurationProperties({ KeyProperties.class })
public class EncryptionBootstrapConfiguration {@Autowired(required = false)// 这个地方会从IOC容器中获取上面我们提到的那个接口的实现类,由于是required = false,所以不一定获取得到,因为可能容器中没有这个对象private TextEncryptor encryptor;@Autowiredprivate KeyProperties key;// 这里 spring IOC 容器添加一个 EnvironmentDecryptApplicationInitializer  组件@Beanpublic EnvironmentDecryptApplicationInitializer environmentDecryptApplicationListener() {     // 这里判断上面注入的 TextEncryptor  对象是否为空if (this.encryptor == null) {//为null,就创建一个默认的this.encryptor = new FailsafeTextEncryptor();}// 否则使用上面注入的那个 TextEncryptor  EnvironmentDecryptApplicationInitializer listener = new EnvironmentDecryptApplicationInitializer(this.encryptor);listener.setFailOnError(this.key.isFailOnError());return listener;}/**省略其他代码,只看关键的*/}

    再看这个 EnvironmentDecryptApplicationInitializer 类的源码:

public class EnvironmentDecryptApplicationInitializer implementsApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {/** 这里的 {cipher} 相当于我们 springboot配置文件解密 的 PASSWORD[]springcloud的配置格式是: '{cipher}BR23C92223KKDNUIQMPLS0009'而我们的配置格式是:               PASSWORD[BR23C92223KKDNUIQMPLS0009]注意: '' 必须要加,不然yaml解析器,解析不了,会报错。*/public static final String ENCRYPTED_PROPERTY_PREFIX = "{cipher}";// 解密的对象private TextEncryptor encryptor;// 构造函数,传入解密对象,前一个配置类传入的public EnvironmentDecryptApplicationInitializer(TextEncryptor encryptor) {// 进行属性赋值this.encryptor = encryptor;}// 这个方法,我们看关键点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 correctlyotherCollectionProperties.put(key, value);}else {// override previously encrypted with non-encrypted propertyproperties.remove(key);}}}// copy all indexed properties even if not encryptedif (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);});}// 这里是真正调用解密方法进行解密了private String decrypt(String key, String original) {String value = original.substring(ENCRYPTED_PROPERTY_PREFIX.length());try {// 这里的 encryptor 对象就是构造函数传入的 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 "";}}
}

    以上两个类的源码,我这里省略了很多,想仔细查看的自己可以去看看这两个类,我这里关键的地方都已经做了注释。

    这里给大家梳理一下流程:

  1. @Configuration标注EncryptionBootstrapConfiguration 类,说明是个配置类
  2. 既然是配置类那么必然是要导入组件到spring中
  3. @Autowired 注入TextEncryptor ,默认IOC容器中是没有的这个对象的,所以注入失败,值为null
  4. TextEncryptor 值为null,就会创建一个默认的 this.encryptor = new FailsafeTextEncryptor();
  5. @Bean 导入EnvironmentDecryptApplicationInitializer 这个组件,构造函数传入 TextEncryptor
  6. 接下来就是找到对应的加密配置项 if (value.startsWith(ENCRYPTED_PROPERTY_PREFIX))
  7. 然后调用 TextEncryptor接口实现对象的decrypt()方法执行解密操作。

    通过上面的分析我们知道解密的关键点就是TextEncryptor,如果我们在加载EncryptionBootstrapConfiguration 配置类之前,给IOC容器中加入一个我们自己实现的解密算法,那么等到注入TextEncryptor 的时候,就不会为空了,也就不会创建默认的FailsafeTextEncryptor对象,那么在解密的时候不就执行我们自己的解密算法了吗?

    现在的问题就是要解决:在何时加入,如何加入这个自己实现的解密算法到IOC容器中,这个时候又想到了spring、springboot、springcloud的各种扩展点了,熟悉这些扩展点的都知道

    ApplicationPreparedEvent 事件,在 BeanFactory 创建完成后,但是还并没有执行refresh()方法的时候,就会发布这个事件,因为我们知道解析配置类是属于refresh()中的一步,所以这样的思路是可行的。

实现代码如下:

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 {@Overridepublic void onApplicationEvent(ApplicationPreparedEvent event) {ConfigurableApplicationContext applicationContext = event.getApplicationContext();// 这里回往spring IOC 中添加好几次,是因为父子容器的原因,所以要判断一下if(applicationContext instanceof AnnotationConfigApplicationContext){ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();// 这里判断是否已经添加过我们自己的解密算法了,没添加才添加,否则跳过if(!beanFactory.containsBean("textEncryptor")){beanFactory.registerSingleton("textEncryptor",new TextEncryptor(){@Overridepublic String encrypt(String text) {System.out.println("=====================================加密");return "加密"+text;}@Overridepublic String decrypt(String encryptedText) {//这里解密就直接输出日志,然后直接解密返回System.out.println("=====================================解密");return EncryptUtil.decrypt("work0", encryptedText);}});}}}@Overridepublic int getOrder() {return Ordered.HIGHEST_PRECEDENCE;}
}

    接下来,就是让我们的监听器生效了,老规矩,在spring.factories中加上这么一句话:
org.springframework.context.ApplicationListener=cn.yjh.listener.RegisterTextEncryptorListener

    这样就可以了,注意配置中心配置加密项的时候一定要注意格式,否则解析不了会报错,正确格式如下:

mysql:driver: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/test?characterEncoding=utf8username: root# BR23C92223KKDNUIQMPLS0009 为经过加密处理的密码,注意一定要加 '' 否则解析yaml会报错  password: '{cipher}BR23C92223KKDNUIQMPLS0009'

总结

    springcloud配置中心解密配置项,也是看源码的时候才发现原来springcloud已经支持了这个功能,以前没看过这一块儿的源码的时候,都不知道可以这么实现,以前都是使用:jasypt-spring-boot-starter来实现的,所以说多看源码还是会有所收获的,这篇文章就到这里,不足之处,还望大家能够指出来,大家共同学习进步。

springboot配置文件密文解密相关推荐

  1. Springboot之Jasypt配置文件加密/解密

    Jasypt配置文件加密/机密 一.Jasypt介绍 二.Springboot整合Jasypt 2.1 环境配置 2.2 添加依赖 2.3 添加Jasypt配置 2.4 编写加/解密工具类 2.5 修 ...

  2. SpringBoot配置文件数据库密码加密

    引言 需求:springboot的配置文件中,把连接数据库的密码加密,使之不是以明文存储 步骤 导入Maven坐标 <dependency><groupId>com.githu ...

  3. jasypt配置文件加解密

    在我们的开发中不可避免的需要使用到一些密码(数据库.redis等),开发和测试环境还好,但生产如果采用明文配置讲会有安全问题. 实现配置的脱敏我使用了Java的一个加解密工具Jasypt,它提供了单密 ...

  4. 如何保护 SpringBoot 配置文件中的敏感信息

    今日推荐 Java 8 一行代码解决了空指针问题,太厉害了...List中remove()方法的陷阱,被坑惨了!25000 字详解 23 种设计模式,原来可以这么简单!最牛逼的 Java 日志框架,性 ...

  5. redismanager 获取不到yml中的密码_SpringBoot敏感信息加密,springboot配置文件密码加密jasypt...

    使用过SpringBoot配置文件的朋友都知道,资源文件中的内容通常情况下是明文显示,安全性就比较低一些.打开application.properties或application.yml,比如mysq ...

  6. SpringBoot 配置文件bootstrap和application的区别

    目录 一.SpringBoot配置文件 二.bootstrap和application区别 三.bootstrap和application的应用场景 一.SpringBoot配置文件 bootstra ...

  7. SpringBoot配置文件敏感信息加密,springboot配置文件数据库密码加密jasypt

    使用过SpringBoot配置文件的朋友都知道,资源文件中的内容通常情况下是明文显示,安全性就比较低一些.打开application.properties或application.yml,比如mysq ...

  8. SpringBoot配置文件yml敏感信息加密

    java项目使用SpringBoot很方便,但SpringBoot的配置文件朋友们都知道,资源文件中的内容通常情况下是明文显示,安全性就比较低一些.打开application.properties或a ...

  9. springBoot配置文件密码加密

    由于信息安全越来越重视,当别人拿到或者看到你的配置文件.相当于数据库等在裸奔. 所以我们需要对敏感信息比如IP,密码,用户名,关键路径等加密. 我们需要加个jar就可以简单实现配置文件密文启动. 添加 ...

  10. springBoot配置文件设置mongodb连接密码加密

    springBoot配置文件设置mongodb连接密码加密 方案 实践 pom文件引入依赖 application.yml文件中定义加密的秘钥 定义main函数测试加解密 把加密后的密码放置到Mong ...

最新文章

  1. hdu5246超级赛亚ACMer
  2. CTFshow 命令执行 web75
  3. Matlab简单系统仿真示例1
  4. php承载,javascript,php_如何防止无限循环的php CPU承载过重?,javascript,php - phpStudy...
  5. Android之解决Android8.0手机(Notification)收不到自定义消息通知以及其它手机得到数据不同步
  6. 计算机等级考试试题4,计算机等级考试二级模拟试题4
  7. (软件工程复习核心重点)第十章面向对象设计-第一节:面向对象设计的基本概念与准则
  8. 笨办法学 Python · 续 练习 44:使用 Python 的数据库 API
  9. 导入别的项目到我的eclipse上出现红色感叹号问题
  10. 如何自定义设置Mac OS系统和windows系统键盘的方法
  11. 在苹果Mac中如何一键转换繁体与简体中文?
  12. 你的AJAX 请求真的安全?
  13. PMP考试教材是什么?有几本?
  14. matlab dll 反编译,libmx.dll
  15. 第八届泰迪杯数据挖掘赛C题总结
  16. 自学考试java语言程序设计_自考java 04747《Java语言程序设计(一)》教材电子版...
  17. adb命令启动某个action_各种启动命令
  18. office中计算机剪贴画,Office 2010的剪贴画
  19. 目标跟踪(1)基于OpenCV实现单目标跟踪
  20. 随机数生成器RandomNumberGenerator

热门文章

  1. Pandas(七)--分组、合并和连接
  2. 【TcaplusDB知识库】读取数据示例(TDR表)
  3. WannaRen勒索软件解密密码计算工具发布
  4. linux脚本解密,shell脚本加密与解密
  5. 爬虫(19)pipline补充+item的讲解+古诗文案例
  6. 【复杂网络】当机器学习遇上复杂网络:解析微信朋友圈 Lookalike 算法
  7. mame4android汉化,MAME4droid
  8. 使用组策略批量禁用u盘
  9. Antd Card study
  10. 北大MBA夫妇不满现有教育系统 携女隐居终南山