关于springboot配置文件密文解密方式

 更新时间:2022年8月16日 18:29  点击:278 作者:weixin_41106708

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

一、配置文件密文解密

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

例如:

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

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

例如:

mysql:
  driver: com.mysql.jdbc.Driver
  url: jdbc:mysql://localhost:3306/test?characterEncoding=utf8
  username: 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> {
    @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("]")) {
                        // 此处进行截取出对应的密文 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;
	@Autowired
	private KeyProperties key;
	
	// 这里 spring IOC 容器添加一个 EnvironmentDecryptApplicationInitializer  组件
	@Bean
	public 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 implements
		ApplicationContextInitializer<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 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);
		});
	}
	
	// 这里是真正调用解密方法进行解密了
	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 "";
		}
	}
}

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

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

  • @Configuration标注EncryptionBootstrapConfiguration 类,说明是个配置类
  • 既然是配置类那么必然是要导入组件到spring中
  • @Autowired 注入TextEncryptor ,默认IOC容器中是没有的这个对象的,所以注入失败,值为null
  • TextEncryptor 值为null,就会创建一个默认的 this.encryptor = new FailsafeTextEncryptor();
  • @Bean 导入EnvironmentDecryptApplicationInitializer 这个组件,构造函数传入 TextEncryptor
  • 接下来就是找到对应的加密配置项 if (value.startsWith(ENCRYPTED_PROPERTY_PREFIX))
  • 然后调用 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 {
    @Override
    public 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(){
                    @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;
    }
}

接下来,就是让我们的监听器生效了,老规矩,在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 为经过加密处理的密码,注意一定要加 '' 否则解析yaml会报错  
  password: '{cipher}BR23C92223KKDNUIQMPLS0009'

总结

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

以上为个人经验,希望能给大家一个参考,也希望大家多多支持猪先飞。

原文出处:https://blog.csdn.net/weixin_41106708/article/details/120263

[!--infotagslink--]

相关文章

  • 解决springboot使用logback日志出现LOG_PATH_IS_UNDEFINED文件夹的问题

    这篇文章主要介绍了解决springboot使用logback日志出现LOG_PATH_IS_UNDEFINED文件夹的问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-04-28
  • SpringBoot实现excel文件生成和下载

    这篇文章主要为大家详细介绍了SpringBoot实现excel文件生成和下载,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-02-09
  • 详解springBoot启动时找不到或无法加载主类解决办法

    这篇文章主要介绍了详解springBoot启动时找不到或无法加载主类解决办法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-09-16
  • SpringBoot集成Redis实现消息队列的方法

    这篇文章主要介绍了SpringBoot集成Redis实现消息队列的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-02-10
  • AES加密解密的例子小结

    关于AES加密的算法我们就不说了,这里主要给各位演示了三个关于AES算法实现的加密例子,希望本文章能给你带来帮助。 话不多说,先放上代码,一共有两个文件:AES.php(aes算...2016-11-25
  • 查找php配置文件php.ini所在路径的二种方法

    通常php.ini的位置在:复制代码 代码如下:/etc目录下或/usr/local/lib目录下。如果你还是找不到php.ini或者找到了php.ini修改后不生效(其实是没找对),请使用如下办法:1.新建php文件,写入如下代码复制代码 代码如下:<?phpe...2014-05-31
  • 解决Springboot get请求是参数过长的情况

    这篇文章主要介绍了解决Springboot get请求是参数过长的情况,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-09-17
  • Spring Boot项目@RestController使用重定向redirect方式

    这篇文章主要介绍了Spring Boot项目@RestController使用重定向redirect方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-09-02
  • Springboot+TCP监听服务器搭建过程图解

    这篇文章主要介绍了Springboot+TCP监听服务器搭建过程,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2020-10-28
  • springBoot 项目排除数据库启动方式

    这篇文章主要介绍了springBoot 项目排除数据库启动方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-09-10
  • php威盾解密的例子分享

    下面来给大家分享两个关于php威盾解密的例子,一个是批量解密一个是超级算法的解密都非常的好,大家有举的进入参考。 例子,批量解密 代码如下 复制代码 ...2016-11-25
  • PHP加密解密函数详解

    分享一个PHP加密解密的函数,此函数实现了对部分变量值的加密的功能。 加密代码如下: /* *功能:对字符串进行加密处理 *参数一:需要加密的内容 *参数二:密钥 */ function passport_encrypt($str,$key){ //加密函数 srand(...2015-10-30
  • php cli配置文件问题分析

    引言今天在教别人使用protobuf的时候,无意中发现了一个php cli模式下的诡异问题,费了老半天的找到解决方法了,这里拿出来分享下。问题描述我们这边最先引入了protobuf协议,使用的是allegro/php-protobuf这个扩展安装的。...2015-10-21
  • C#中读写INI配置文件的方法

    这篇文章主要介绍了C#中读写INI配置文件的方法,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下...2020-06-25
  • 详解SpringBoot之访问静态资源(webapp...)

    这篇文章主要介绍了详解SpringBoot之访问静态资源(webapp...),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-09-14
  • SpringBoot接口接收json参数解析

    这篇文章主要介绍了SpringBoot接口接收json参数解析,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教...2021-10-19
  • springboot中使用@Transactional注解事物不生效的坑

    这篇文章主要介绍了springboot中使用@Transactional注解事物不生效的原因,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-01-26
  • C# 获取硬盘号,CPU信息,加密解密技术的步骤

    这篇文章主要介绍了C# 获取硬盘号,CPU信息,加密解密技术的步骤,帮助大家更好的理解和学习c#,感兴趣的朋友可以了解下...2021-01-16
  • C#实现对文件进行加密解密的方法

    这篇文章主要介绍了C#实现对文件进行加密解密的方法,涉及C#加密与解密的技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25
  • springboot多模块包扫描问题的解决方法

    这篇文章主要介绍了springboot多模块包扫描问题的解决方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-09-16