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

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

在使用 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来实现的,所以说多看源码还是会有所收获的,这篇文章就到这里。

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

(0)

相关推荐

  • Spring boot配置文件加解密详解

    功能介绍 大家都知道在Spring boot开发过程中,需要在配置文件里配置许多信息,如数据库的连接信息等,如果不加密,传明文,数据库就直接暴露了,相当于"裸奔"了,因此需要进行加密处理才行. 在项目中使用jasypt-1.9.4.jar包,能够实现对明文进行加密,对密文进行解密.配置相关加密信息,就能够实现在项目运行的时候,自动把配置文件中已经加密的信息解密成明文,供程序使用 下面话不多说了,来一起看看详细的介绍吧 使用说明 1.pom引入依赖 <dependency>

  • Spring Boot 实现配置文件加解密原理

    背景 接上文<失踪人口回归,mybatis-plus 3.3.2 发布>[1] ,提供了一个非常实用的功能 「数据安全保护」 功能,不仅支持数据源的配置加密,对于 spring boot 全局的 yml /properties 文件均可实现敏感信息加密功能,在一定的程度上控制开发人员流动导致敏感信息泄露. // 数据源敏感信息加密 spring: datasource: url: mpw:qRhvCwF4GOqjessEB3G+a5okP+uXXr96wcucn2Pev6BfaoEMZ1gVp

  • 关于Springboot数据库配置文件明文密码加密解密的问题

    有时候因为安全问题,需要把配置文件的中数据库用户名密码由明文改成密文,大多数其实是为了应付甲方而已. 1.pom.xml引入依赖 <dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>2.1.0</version> </dependenc

  • Spring Boot 在启动时进行配置文件加解密

    寻找到application.yml的读取的操作. 从spring.factories 中查看到 # Application Listeners org.springframework.context.ApplicationListener=\ org.springframework.boot.context.config.ConfigFileApplicationListener,\ ConfigFileApplicationListener 该对象对application.yml进行读取操作

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

    目录 一.配置文件密文解密 二.配置中心密文解密( 以springcloud+nacos为例 ) 总结 在使用 springboot 或者 springcloud 开发的时候,通常为了保证系统的安全性,配置文件中的密码等铭感信息都会进行加密处理,然后在系统启动的时候对密文进行解密处理. 一.配置文件密文解密 在使用 springboot 或者 springcloud 的时候,通常会在 application.yaml 配置文件中配置数据库的连接信息. 例如: mysql: driver: com

  • 浅谈SpringBoot主流读取配置文件三种方式

    读取配置SpringBoot配置文件三种方式 一.利用Bean注解中的Value(${})注解 @Data @Component public class ApplicationProperty { @Value("${application.name}") private String name; } 该方式可以自动读取当前配置文件appliation.yml  或者application.properties中的配置值 区别在于读取yml文件时候支持中文编码,peoperties需

  • 关于springboot 配置文件中属性变量引用方式@@解析

    这种属性应用方式是 field_name=@field_value@. 两个@符号是springboot为替代${}属性占位符产生,原因是${}会被maven处理,所以应该是起不到引用变量的作用. @@方式可以引用springboot非默认配置文件(即其他配置文件)中的变量: springboot默认配置文件是 src/main/resources/application.properties 补充知识:springboot项目使用@Value注解获取配置文件中的配置信息 application

  • SpringBoot配置加载,各配置文件优先级对比方式

    目录 1.SpringBoot配置文件 以设置应用端口为例 2.配置文件目录 3.自定义配置属性 自定义配置提示 4.指定配置文件 @PropertySource使用 装配yaml配置文件 @ImportResource使用 其他问题 idea使用*.properties文件出现中文乱码问题? 解决方法: 1.SpringBoot配置文件 SpringBoot使用一个以application命名的配置文件作为默认的全局配置文件.支持properties后缀结尾的配置文件或者以yml/yaml后缀

  • SpringBoot框架配置文件路径设置方式

    目录 SpringBoot配置文件路径设置 自定义配置文件路径以及多profile配置文件 一.什么是classpath 二.自定义springboot配置文件路径 三.多 profiles 配置文件的切换 SpringBoot配置文件路径设置 选择"Edit Configurations": 下springboot启动配置中修改VM options的值: 值参考: -Dspring.config.location=E:/workspace/xxxx/application.yml -

  • SpringBoot 配置文件给实体注入值方式

    目录 配置文件给实体注入值 properties用法 yaml用法 通过配置文件给实体类注入属性 上面yml中注意 也可以使用properties注入 配置文件值注入方式 1.1 从全局配置文件中读取配置到实体类 (@ConfigurationProperties) 1.2 全局配置文件注入值(@Value) 1.3 从指定文件读取并配置实体类(@PropertySource+@ConfigurationProperties) 1.4 从指定文件读取并注入值(@PropertySource+@V

  • SpringBoot加载配置文件的实现方式总结

    目录 一.简介 二.代码实践 2.1.通过@value注解实现参数加载 2.2.通过@ConfigurationProperties注解实现参数加载 2.3.通过@PropertySource注解实现配置文件加载 2.4.通过自定义环境处理类,实现配置文件的加载 2.5.最后,我们来介绍一下yml文件读取 三.小结 一.简介 在实际的项目开发过程中,我们经常需要将某些变量从代码里面抽离出来,放在配置文件里面,以便更加统一.灵活的管理服务配置信息.比如,数据库.eureka.zookeeper.r

  • SpringBoot配置文件中数据库密码加密两种方案(推荐)

    SpringBoot项目经常将连接数据库的密码明文放在配置文件里,安全性就比较低一些,尤其在一些企业对安全性要求很高,因此我们就考虑如何对密码进行加密. 介绍两种加密方式:jasypt 可加密配置文件中所有属性值; druid 自带了加解密,可对数据库密码进行加密. jasypt 加解密 jasypt 是一个简单易用的加解密Java库,可以快速集成到 Spring 项目中.可以快速集成到 Spring Boot 项目中,并提供了自动配置,使用非常简单. 步骤如下: 1)引入maven依赖 <de

  • Jasypt对SpringBoot配置文件加密

    引入maven <dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency> 生成加密串 将连接数据库的用户名和密码进行加密 public static void main(Str

  • SpringBoot配置文件中密码属性加密的实现

    目录 背景 集成jasypt-spring-boot到项目中 配置文件配置加密与读取 工作原理简析 使用自定义的加密算法 结语 本文主要介绍了SpringBoot配置文件中的明文密码如何加密保存,读取以及对于自定义的加密算法加密的参数如何保存和读取. 背景 为了安全的需要,一些重要的信息比如数据库密码不能明文保存在配置文件中,需要进行加密之后再保存.SpringBoot可以使用jasypt-spring-boot这个组件来为配置属性提供加密的支持. 集成jasypt-spring-boot到项目

随机推荐