Spring Boot配置文件敏感信息加密
一,背景
Spring Boot应用中的数据库、Redis、Nacos、MQ等的用户名、连接地址、密码在配置文件中一般都是明文存储,如果系统被系统攻破或者配置文件所在的目录读权限被破解,又或者是动态配置文件被窃取,内部人员或者黑客很容易通过配置文件获取到数据库的用户名和密码,进而达到非法连接数据库盗取数据的目的。
本文的目标是对配置文件的敏感信息加密,同时保持对现有应用的最小改动,对应用中的配置文件中的秘文配置项的使用保持和加密前一致,也就是使用配置项不受到任何影响。
二,SpringBoot自动配置原理分析
2.1 配置文件加载准备
- 我们知道SpringApplication构造器加载完Initialzers和Listenter后开始调用run(String…args)方法启动Springboot上下文。
/*** Run the Spring application, creating and refreshing a new* {@link ApplicationContext}.* @param args the application arguments (usually passed from a Java main method)* @return a running {@link ApplicationContext}*/public ConfigurableApplicationContext run(String... args) {long startTime = System.nanoTime();DefaultBootstrapContext bootstrapContext = createBootstrapContext();ConfigurableApplicationContext context = null;configureHeadlessProperty();SpringApplicationRunListeners listeners = getRunListeners(args);listeners.starting(bootstrapContext, this.mainApplicationClass);try {ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);// 配置文件加载入口,它会去执行SpringApplication构造器加载到ListerConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);Banner printedBanner = printBanner(environment);context = createApplicationContext();context.setApplicationStartup(this.applicationStartup);prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);refreshContext(context);afterRefresh(context, applicationArguments);Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);if (this.logStartupInfo) {new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);}listeners.started(context, timeTakenToStartup);callRunners(context, applicationArguments);}catch (Throwable ex) {if (ex instanceof AbandonedRunException) {throw ex;}handleRunFailure(context, ex, listeners);throw new IllegalStateException(ex);}try {if (context.isRunning()) {Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);listeners.ready(context, timeTakenToReady);}}catch (Throwable ex) {if (ex instanceof AbandonedRunException) {throw ex;}handleRunFailure(context, ex, null);throw new IllegalStateException(ex);}return context;}
-
- SpringApplication#prepareEnvironment( listeners, applicationArguments), 这个方法是配置文件加载路口,他会执行SpringApplication构造器加载到Listener。这里我们重要关注BootstrapApplicationListener和ConfigFileApplicationListener这两个监听器。
package org.springframework.boot.SpringApplication;private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {// 给容器创建一个 environmentConfigurableEnvironment environment = getOrCreateEnvironment();configureEnvironment(environment, applicationArguments.getSourceArgs());ConfigurationPropertySources.attach(environment);// 执行引入jar包类路径下的META/INF/spring.factories文件到监听器listeners.environmentPrepared(bootstrapContext, environment);DefaultPropertiesPropertySource.moveToEnd(environment);Assert.state(!environment.containsProperty("spring.main.environment-prefix"),"Environment prefix cannot be set via properties.");// 将加载完成的环境变量信息绑定到Spring IOC容器中bindToSpringApplication(environment);if (!this.isCustomEnvironment) {EnvironmentConverter environmentConverter = new EnvironmentConverter(getClassLoader());environment = environmentConverter.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());}ConfigurationPropertySources.attach(environment);return environment;}
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {ResolvableType type = eventType != null ? eventType : this.resolveDefaultEventType(event);Executor executor = this.getTaskExecutor();Iterator var5 = this.getApplicationListeners(event, type).iterator();while(var5.hasNext()) {ApplicationListener<?> listener = (ApplicationListener)var5.next();if (executor != null) {executor.execute(() -> {this.invokeListener(listener, event);});} else {// 触发BootstrapConfigFileApplicationListenerthis.invokeListener(listener, event);}}}
-
- SpringApplication#prepareEnvironment()触发执行监听器,优先执行BootstrapApplicationListener监听器,再执行ConfigFileApplicationListener监听器
- BootstrapApplicationListener:来自SpringCloud。优先级最高,用于启动/建立Springcloud的应用上下文。需要注意的是,到此时Springboot的上下文还未创建完成,因为在创建springboot上下文的时候通过BootstrapApplicationListener去开启了springcloud上下文的创建流程。这个流程“嵌套”特别像是Bean初始化流程:初始化A时,A->B,就必须先去完成Bean B的初始化,再回来继续完成A的初始化。
- 在建立SpringCloud的应用的时候,使用的也是SpringApplication#run()完成的,所以也会走一整套SpringApplication的生命周期逻辑。这里之前就踩过坑,初始化器、监听器等执行多次,若只需执行一次,需要自行处理。
- Springcloud和Springboot应用上下文都是使用ConfigFileApplicationListener来完成加载和解析的
Springboot应用上下文读取的是配置文件默认是:application
Springcloud应用上下文读取的外部配置文件名默认是:bootstrap
- BootstrapApplicationListener 核心代码
@Overridepublic void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {// 检查是否开启了SpringCloudConfigurableEnvironment environment = event.getEnvironment();if (!bootstrapEnabled(environment) && !useLegacyProcessing(environment)) {return;}// don't listen to events in a bootstrap context// 如果执行了Springcloud上下文触发的BootStapApplicationListener这个监听器,就不执行这个监听器了 避免重复执行if (environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {return;}ConfigurableApplicationContext context = null;String configName = environment.resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");for (ApplicationContextInitializer<?> initializer : event.getSpringApplication().getInitializers()) {if (initializer instanceof ParentContextApplicationContextInitializer) {context = findBootstrapContext((ParentContextApplicationContextInitializer) initializer, configName);}}// 如果还未创建SpringCloud上下文实例,则调用bootstrapServiceContextif (context == null) {context = bootstrapServiceContext(environment, event.getSpringApplication(), configName);event.getSpringApplication().addListeners(new CloseContextOnFailureApplicationListener(context));}apply(context, event.getSpringApplication(), environment);}
- BootstrapApplicationListener#bootstrapServiceContext()核心源码如下
private ConfigurableApplicationContext bootstrapServiceContext(ConfigurableEnvironment environment,final SpringApplication application, String configName) {ConfigurableEnvironment bootstrapEnvironment = new AbstractEnvironment() {};MutablePropertySources bootstrapProperties = bootstrapEnvironment.getPropertySources();String configLocation = environment.resolvePlaceholders("${spring.cloud.bootstrap.location:}");String configAdditionalLocation = environment.resolvePlaceholders("${spring.cloud.bootstrap.additional-location:}");Map<String, Object> bootstrapMap = new HashMap<>();bootstrapMap.put("spring.config.name", configName);// if an app (or test) uses spring.main.web-application-type=reactive, bootstrap// will fail// force the environment to use none, because if though it is set below in the// builder// the environment overrides itbootstrapMap.put("spring.main.web-application-type", "none");if (StringUtils.hasText(configLocation)) {bootstrapMap.put("spring.config.location", configLocation);}if (StringUtils.hasText(configAdditionalLocation)) {bootstrapMap.put("spring.config.additional-location", configAdditionalLocation);}bootstrapProperties.addFirst(new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap));for (PropertySource<?> source : environment.getPropertySources()) {if (source instanceof StubPropertySource) {continue;}bootstrapProperties.addLast(source);}// TODO: is it possible or sensible to share a ResourceLoader?// 通过SpringApplicationBuilder构建一个SpringCloud的上下文实例SpringApplicationBuilder builder = new SpringApplicationBuilder().profiles(environment.getActiveProfiles()).bannerMode(Mode.OFF).environment(bootstrapEnvironment)// Don't use the default properties in this builder.registerShutdownHook(false).logStartupInfo(false).web(WebApplicationType.NONE);final SpringApplication builderApplication = builder.application();if (builderApplication.getMainApplicationClass() == null) {// gh_425:// SpringApplication cannot deduce the MainApplicationClass here// if it is booted from SpringBootServletInitializer due to the// absense of the "main" method in stackTraces.// But luckily this method's second parameter "application" here// carries the real MainApplicationClass which has been explicitly// set by SpringBootServletInitializer itself already.builder.main(application.getMainApplicationClass());}if (environment.getPropertySources().contains("refreshArgs")) {// If we are doing a context refresh, really we only want to refresh the// Environment, and there are some toxic listeners (like the// LoggingApplicationListener) that affect global static state, so we need a// way to switch those off.builderApplication.setListeners(filterListeners(builderApplication.getListeners()));}builder.sources(BootstrapImportSelectorConfiguration.class);// 调用Springcloud上下文实例的run方法,使用的也是SpringApplication#run()方法//这个过程会将之前的步骤在执行一次final ConfigurableApplicationContext context = builder.run();// gh-214 using spring.application.name=bootstrap to set the context id via// `ContextIdApplicationContextInitializer` prevents apps from getting the actual// spring.application.name// during the bootstrap phase.context.setId("bootstrap");// Make the bootstrap context a parent of the app contextaddAncestorInitializer(application, context);// It only has properties in it now that we don't want in the parent so remove// it (and it will be added back later)bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);return context;}
- 调用springcloud上下文实例的run方法,会将之前的步骤在重复执行一次,程序又执行到遍历监听器并发这里了,重点关注一下ConfigFileApplicationListener,这个监听器会完成配置文件的加载。
- 进入断点里面之前,我先做一些说明。因为这里会创建Springcloud和Springboot两个上下文实例, 由于Springboot和Springcloud上下文实例加载配置文件的流程都是相似的,这里我们就讲解Springboot容器配置文件的加载过程。
- bootstrap.yml 可以用来定义应用级别的, 应用程序特有配置信息,可以用来配置后续各个模块中需使用的公共参数等。
- 如果application.yml的内容标签与bootstrap的标签一致,application会覆盖bootstrap, 而application.yml 里面的内容可以动态替换。
2.2 配置文件加载解析
- EnvironmentPostProcessorApplicationListener#onApplicationEnvironmentPreparedEvent(),根据上面的流程可知,程序会触发EnvironmentPostProcessorApplicationListener的onApplicationEvent方法,从而加载配置文件。
获取所有的onApplicationEnvironmentPreparedEvent后置处理器,并执行后置处理器方法
@Overridepublic void onApplicationEvent(ApplicationEvent event) {if (event instanceof ApplicationEnvironmentPreparedEvent environmentPreparedEvent) {onApplicationEnvironmentPreparedEvent(environmentPreparedEvent);}if (event instanceof ApplicationPreparedEvent) {onApplicationPreparedEvent();}if (event instanceof ApplicationFailedEvent) {onApplicationFailedEvent();}}private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {ConfigurableEnvironment environment = event.getEnvironment();SpringApplication application = event.getSpringApplication();for (EnvironmentPostProcessor postProcessor : getEnvironmentPostProcessors(application.getResourceLoader(),event.getBootstrapContext())) {postProcessor.postProcessEnvironment(environment, application);}}
@Overridepublic void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {if (PropertyUtils.bootstrapEnabled(environment)) {addPropertySources(environment, application.getResourceLoader());}}/*** Add config file property sources to the specified environment.* @param environment the environment to add source to* @param resourceLoader the resource loader* @see #addPostProcessors(ConfigurableApplicationContext)*/protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {RandomValuePropertySource.addToEnvironment(environment);new Loader(environment, resourceLoader).load();}
- BootstrapConfigFileApplicationListener#addPropertySources(),流程继续执行到addPropertySources,这里会去新建一个Loader内部类,并执行load方法。
protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {RandomValuePropertySource.addToEnvironment(environment);new Loader(environment, resourceLoader).load();}
- BootstrapConfigFileApplicationListener#Loader#load()方法
private class Loader {private final Log logger = BootstrapConfigFileApplicationListener.this.logger;private final ConfigurableEnvironment environment;private final PropertySourcesPlaceholdersResolver placeholdersResolver;private final ResourceLoader resourceLoader;private final List<PropertySourceLoader> propertySourceLoaders;private Deque<Profile> profiles;private List<Profile> processedProfiles;private boolean activatedProfiles;private Map<Profile, MutablePropertySources> loaded;private Map<DocumentsCacheKey, List<Document>> loadDocumentsCache = new HashMap<>();Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {this.environment = environment;this.placeholdersResolver = new PropertySourcesPlaceholdersResolver(this.environment);this.resourceLoader = (resourceLoader != null) ? resourceLoader : new DefaultResourceLoader(null);this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,this.resourceLoader.getClassLoader());}void load() {FilteredPropertySource.apply(this.environment, DefaultPropertiesPropertySource.NAME, LOAD_FILTERED_PROPERTY,this::loadWithFilteredProperties);}
- 初始化initializeProfiles
private void initializeProfiles() {// The default profile for these purposes is represented as null. We add it// first so that it is processed first and has lowest priority.//默认添加一个null,这样的目的是为了先出来加载application.xxx文件,优先级最低this.profiles.add(null);// 把当前environment中已经加载的系统级别的配置文件包装到Binder容器中Binder binder = Binder.get(this.environment);// 在Binder容器中找到spring.profiles.actives配置列表Set<Profile> activatedViaProperty = getProfiles(binder, ACTIVE_PROFILES_PROPERTY);// 在Binder容器中找到spring.profiles.include配置列表Set<Profile> includedViaProperty = getProfiles(binder, INCLUDE_PROFILES_PROPERTY);//environment的spring.profiles.active属性中存在且activeViaProperty和includedViaProProperty Property不存在的配置List<Profile> otherActiveProfiles = getOtherActiveProfiles(activatedViaProperty, includedViaProperty);// 将解析出的profile依次按照otherActiveProfiles、includeViaProperty和activeViaProperty的先后次序//将添加进去的先被加载,但Spring读取使用优先级最低,因为最后一次进行reverse操作this.profiles.addAll(otherActiveProfiles);// Any pre-existing active profiles set via property sources (e.g.// System properties) take precedence over those added in config files.this.profiles.addAll(includedViaProperty);addActiveProfiles(activatedViaProperty);// 在系统中未加载到的profile,此时profiles中就只有进入此方法默认添加的null// 此时就给profile添加一个“default”,若在application.xxx中仍未配置指定的profile则会去加载此时添加的“default”//若application.xxx中配置了指定的profile则会将“default”从profile移除if (this.profiles.size() == 1) { // only has null profilefor (String defaultProfileName : getDefaultProfiles(binder)) {Profile defaultProfile = new Profile(defaultProfileName, true);this.profiles.add(defaultProfile);}}}
- 根据源码调用链路可知,程序继续调用Loader#load( profile, filterFactory, consumer)
- Loader#load(location, name, profile, filterFactory, consumer)
- location:总共分为"classpath:/,classpath:/config/,file:./,file:./config/",配置文件可配置的地址,加载优先级为倒序。
- name:默认为“application”。
- profile:若当前解析的不是spring.profiles.active指定的配置文件时默认为“null”,否则为- spring.profiles.active指定的值。
- filterFactory:
- consumer:将加载的document添加到Loader#loaded属性集合中,用于最后的配置文件优先级排序。
private void load(String location, String name, Profile profile,DocumentFilterFactory filterFactory, DocumentConsumer consumer) {if (!StringUtils.hasText(name)) {for (PropertySourceLoader loader : this.propertySourceLoaders) {if (canLoadFileExtension(loader, location)) {load(loader, location, profile,filterFactory.getDocumentFilter(profile), consumer);return;}}}// 临时存储判断是否已经加载过了某种扩展名类型(propertis、xml、yml、yaml)// 的的配置,避免重复加载Set<String> processed = new HashSet<>();// this.propertySourceLoaders,分为PropertiesPropertySourceLoader和YamlPropertySourceLoader两种// PropertiesPropertySourceLoader:解析properties、xml类型配置// YamlPropertySourceLoader:解析yml、yaml类型for (PropertySourceLoader loader : this.propertySourceLoaders) {// fileExtension由loder类型决定,优先级顺序为properties > xml > yml > ymal// 配置文件拼接规则:location + name + "-" + profile + fileExtension;for (String fileExtension : loader.getFileExtensions()) {if (processed.add(fileExtension)) {loadForFileExtension(loader, location + name, "." + fileExtension,profile, filterFactory, consumer);}}}
}
- Loader#load(loader, location, profile,filter, consumer)核心解析方法,根据已拼接好地址去获取配置文件(例如:classpath:/application-dev.yml)
- 文件不存在:结束当前方法,继续执行下一次循环
- 文件存在:解析配置文件,将解析到的配置文件保存到Loader#loaded变量中
- 文件存在时还需要尝试获取spring.profiles.active属性,规则如下
1,若没有配置该属性值,则加载完当前fileExtension类型的配置(eg: application.properties、xml、yml、yaml)后就不再尝试解析其他fileExtension类型的配置文件了,此时系统就默认使用加载到的application.properties/yml配置2,若配置了该属性值,则读取该属性值(当前配置的是dev),将其添加到Loader+profiles属性中(就是第三步while循环的那个profiles变量值),同时Loader会将activatedProfiles属性值改为true来标记系统已有active这个属性值,Loader也不会再去解析该配置文件了
private void load(PropertySourceLoader loader, String location, Profile profile, DocumentFilter filter, DocumentConsumer consumer) {try {// 根据拼接的配置文件地址来加载配置文件// 例如location为classpath:application.ymlResource resource = this.resourceLoader.getResource(location);// 配置文件不存在,则返回继续查找if (resource == null || !resource.exists()) {if (this.logger.isTraceEnabled()) {StringBuilder description = getDescription("Skipped missing config ", location, resource, profile);this.logger.trace(description);}return;}String name = "applicationConfig: [" + location + "]";// 解析配置文件,并读取spring.profiles.active属性,将读取到的active属性赋值给document.getActiveProfiles()List<Document> documents = loadDocuments(loader, name, resource);// 保存已解析的配置文件List<Document> loaded = new ArrayList<>();for (Document document : documents) {if (filter.match(document)) {// 1、将解析到的spring.profiles.active添加到profiles中,下一次while循环就解析profile// 比如说这里的active为dev,则接下来就拼接并加载dev的配置文件// 2、将activatedProfiles属性设置为true,标注已经解析到了active属性,后续// 就算在后面的配置文件中解析到active属性也不会再加载改配置// 3、移除profiles中的“default”配置,后续将不会再加载application-defalut.yml配置addActiveProfiles(document.getActiveProfiles());// 将本次配置文件中加载到的“spring.profiles.include”中配置profile添加到profiles队列头部// 队列头部的配置将会先被加载,但配置使用的优先级低于后面加载的配置文件(因为配置文件加载完后会执行reverse操作)addIncludedProfiles(document.getIncludeProfiles());// 添加到已加载的配置文件loaded.add(document);}}Collections.reverse(loaded);if (!loaded.isEmpty()) {// 将加载的document添加到Loader#loaded属性集合中,用于最后的配置文件优先级排序// 根据当前加载顺序进行倒序排,由于application.yml比application-dev.yml// 先加载,所以倒序后指定的application-dev.yml配置优先级更高loaded.forEach((document) -> consumer.accept(profile, document));if (this.logger.isDebugEnabled()) {StringBuilder description = getDescription("Loaded config file ",location, resource, profile);this.logger.debug(description);}}} catch (Exception ex) {throw new IllegalStateException("Failed to load property "+ "source from location '" + location + "'", ex);}
}
- 经过上面的步骤将所有的配置文件解析并添加到Loader#loaded属性中后,继续执行第三步中addLoadedPropertySource()方法,该方法会将现有loaded中保存的配置文件倒叙后依次添加到environment中。
private void addLoadedPropertySources() {// 获取环境变量中已加载的配置信息MutablePropertySources destination = this.environment.getPropertySources();// 获取已本次Loader加载到的配置文件List<MutablePropertySources> loaded = new ArrayList<>(this.loaded.values());// 将已加载的配置文件倒序,更改优先级,spring.profile.active指定的优先级最高Collections.reverse(loaded);// 标注上一个添加到environment中的配置文件,用于确定当前配置文件插入的位置String lastAdded = null;// 利用set集合的属性,避免配置文件的重复添加Set<String> added = new HashSet<>();// 遍历并将配置添加到environment中for (MutablePropertySources sources : loaded) {for (PropertySource<?> source : sources) {if (added.add(source.getName())) {// 将已加载的配置文件添加到environment的MutablePropertySources中addLoadedPropertySource(destination, lastAdded, source);lastAdded = source.getName();}}}
}private void addLoadedPropertySource(MutablePropertySources destination, String lastAdded, PropertySource<?> source) {if (lastAdded == null) {// 如果系统中存在"defaultProperties"这个配置,则将第一个优先级的配置文件添加到这个配置文件的顺序之前// 如果系统中不存在"defaultProperties"这个配置,则将第一个优先级的配置文件添加到environment中的最后一个// defaultProperties实际为bootstrap.ymlif (destination.contains(DEFAULT_PROPERTIES)) {destination.addBefore(DEFAULT_PROPERTIES, source);} else {destination.addLast(source);}} else {// 将当前配置文件添加到上一个配置文件之后destination.addAfter(lastAdded, source);}
}
三 通过PropertySourceLoader 实现自定义load配置文件的方式植入加解密
3.1 重写yaml格式的文件加载类
- 新建文件 resourece/META-INF/spring.factories
org.springframework.boot.env.PropertySourceLoader=\
com.ksher.framework.secret.parser.SecretYamlPropertySourceLoader
- 重写yamlSourceLoader
package com.ksher.framework.secret.parser;import com.alibaba.cloud.nacos.parser.AbstractPropertySourceLoader;
import com.ksher.framework.secret.kms.KmsSecret;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.core.Ordered;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.util.ClassUtils;import java.io.IOException;
import java.util.*;public class SecretYamlPropertySourceLoader extends AbstractPropertySourceLoaderimplements Ordered {private static final Logger log = LoggerFactory.getLogger(SecretYamlPropertySourceLoader.class);/*** Get the order value of this object.* <p>* Higher values are interpreted as lower priority. As a consequence, the object with* the lowest value has the highest priority (somewhat analogous to Servlet* {@code load-on-startup} values).* <p>* Same order values will result in arbitrary sort positions for the affected objects.* @return the order value* @see #HIGHEST_PRECEDENCE* @see #LOWEST_PRECEDENCE*/@Overridepublic int getOrder() {return Integer.MIN_VALUE;}@Overridepublic String[] getFileExtensions() {return new String[] { "yml", "yaml" };}@Overridepublic List<PropertySource<?>> load(String name, Resource resource) throws IOException {if (!ClassUtils.isPresent("org.yaml.snakeyaml.Yaml", getClass().getClassLoader())) {throw new IllegalStateException("Attempted to load " + name + " but snakeyaml was not found on the classpath");}List<Map<String, Object>> loaded = new OriginTrackedYamlLoader(resource).load();if (loaded.isEmpty()) {return Collections.emptyList();}List<PropertySource<?>> propertySources = new ArrayList<>(loaded.size());for (int i = 0; i < loaded.size(); i++) {String documentNumber = (loaded.size() != 1) ? " (document #" + i + ")" : "";Map<String, Object> stringObjectMap = loaded.get(i);for (String s : stringObjectMap.keySet()) {try {String value = stringObjectMap.get(s).toString();if (value.startsWith("Encrypted:")) {int prefixLength = "Encrypted:".length();String extractedString = value.substring(prefixLength);String decrypt = KmsSecret.dncrypt(extractedString);stringObjectMap.put(s, decrypt);}} catch (Exception e) {log.error("KmsSecret decrypt failed", e);}}log.info("loaded properties is {}", stringObjectMap);propertySources.add(new OriginTrackedMapPropertySource(name + documentNumber,stringObjectMap, true));}return propertySources;}@Overrideprotected List<PropertySource<?>> doLoad(String name, Resource resource) throws IOException {if (!ClassUtils.isPresent("org.yaml.snakeyaml.Yaml", getClass().getClassLoader())) {throw new IllegalStateException("Attempted to load " + name + " but snakeyaml was not found on the classpath");}List<Map<String, Object>> loaded = new OriginTrackedYamlLoader(resource).load();if (loaded.isEmpty()) {return Collections.emptyList();}List<PropertySource<?>> propertySources = new ArrayList<>(loaded.size());for (int i = 0; i < loaded.size(); i++) {String documentNumber = (loaded.size() != 1) ? " (document #" + i + ")" : "";propertySources.add(new OriginTrackedMapPropertySource(name + documentNumber,Collections.unmodifiableMap(loaded.get(i)), true));}return propertySources;}
}
- 植入解密
@Overridepublic List<PropertySource<?>> load(String name, Resource resource) throws IOException {if (!ClassUtils.isPresent("org.yaml.snakeyaml.Yaml", getClass().getClassLoader())) {throw new IllegalStateException("Attempted to load " + name + " but snakeyaml was not found on the classpath");}List<Map<String, Object>> loaded = new OriginTrackedYamlLoader(resource).load();if (loaded.isEmpty()) {return Collections.emptyList();}List<PropertySource<?>> propertySources = new ArrayList<>(loaded.size());for (int i = 0; i < loaded.size(); i++) {String documentNumber = (loaded.size() != 1) ? " (document #" + i + ")" : "";Map<String, Object> stringObjectMap = loaded.get(i);for (String s : stringObjectMap.keySet()) {try {String value = stringObjectMap.get(s).toString();if (value.startsWith("Encrypted:")) {// 解密植入int prefixLength = "Encrypted:".length();String extractedString = value.substring(prefixLength);
// AESUtil.decrypt(extractedString)String decrypt = KmsSecret.dncrypt(extractedString);stringObjectMap.put(s, decrypt);}} catch (Exception e) {log.error("KmsSecret decrypt failed", e);}}log.info("loaded properties is {}", stringObjectMap);propertySources.add(new OriginTrackedMapPropertySource(name + documentNumber,stringObjectMap, true));}return propertySources;}
- 解析yaml文件
test:secret:mysql:# 加密数据user: Encrypted: E2Z3gvXufRIwuif2RfgkNQ==demo: Encrypted: Encrypted: E2Z3gvXufRIwuif2RfgkNQ==password: Encrypted: E2Z3gvXufRIwuif2RfgkNQ==
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://10.10.7.11:3306/ksher_config_dev?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=trueusername: Encrypted: E2Z3gvXufRIwuif2RfgkNQ==password: Encrypted: E2Z3gvXufRIwuif2RfgkNQ==
- 放入springboot的环境变量中
没有解密前数据
四,github地址
secret-spring-boot-starter
相关文章:

Spring Boot配置文件敏感信息加密
一,背景 Spring Boot应用中的数据库、Redis、Nacos、MQ等的用户名、连接地址、密码在配置文件中一般都是明文存储,如果系统被系统攻破或者配置文件所在的目录读权限被破解,又或者是动态配置文件被窃取,内部人员或者黑客很容易通过…...

Java操作数栈分析
Java 的操作数栈(Operand Stack)是 JVM 的运行时数据区域之一,位于每个线程的栈帧中。操作数栈用于临时存储操作的中间结果和数据(操作数),在方法执行时,JVM 的字节码指令会对操作数栈进行操作。…...

C#|.net core 基础 - 值传递 vs 引用传递
不知道你在开发过程中有没有遇到过这样的困惑:这个变量怎么值被改?这个值怎么没变? 今天就来和大家分享可能导致这个问题的根本原因值传递 vs 引用传递。 在此之前我们先回顾两组基本概念: 值类型** vs 引用类型** **值类型&a…...

axure的下载,激活,汉化全过程,多图
1.前言 下载地址:https://pan.baidu.com/s/12xo1mJer2hmBK7QrYM5v-Q?pwd0107#list/path%2Fcsdn%E5%85%B1%E4%BA%AB%E6%96%87%E4%BB%B6 源文章:https://blog.csdn.net/iwanttostudyc/article/details/123773796?ops_request_misc%257B%2522request%25…...

LCR 026
题目:LCR 026 解法一:线性表 将链表中所有元素加入数组中,创建两个指针,分别指向数组的头部和尾部,然后向中间遍历 public void reorderList(ListNode head) {if (head null || head.next null || head.next.next …...

万能小程序运营管理系统 _requestPost 任意文件读取漏洞复现
0x01 产品简介 万能小程序运营管理系统是一种功能全面的系统,旨在帮助开发者和运营人员更好地管理和推广小程序。该系统集成了多种功能模块,覆盖了从小程序开发、部署到运营管理的全链条服务。系统通过提供丰富的功能和工具,帮助用户轻松搭建、管理和优化小程序。该系统支持…...

libyuv之linux编译
文章目录 一、下载源码二、编译源码三、注意事项1、银河麒麟系统(aarch64)(1)解决 armv8-adotprodi8mm 指令集支持问题(2)解决 armv9-asve2 指令集支持问题 一、下载源码 到GitHub网站下载https://github.…...

vue3路由基本使用
在 Vue 3 中,路由指的是应用程序的导航系统,允许你在不同的视图或页面之间进行切换。通过 vue-router 插件,你可以定义路由规则,将 URL 路径映射到 Vue 组件,实现页面间的跳转和状态管理。使用路由,用户可以…...

哪些人适合学习人工智能?
人工智能(AI)的浪潮正席卷全球,它不仅是科技领域的一场革命,更是社会进步的重要推手。随着AI技术的不断成熟和应用领域的不断拓展,越来越多的人开始关注并渴望掌握这一前沿技术。那么,究竟哪些人适合学习人…...

计算机的错误计算(九十七)
摘要 讨论 的计算精度问题。 由计算机的错误计算(九十六)知,IEEE754-2019标准中含有 运算。 另外,似乎没有语言直接编程实现内置了该运算。 例1. 已知 x-0.9999999999076 . 计算 不妨用 Python的 math库与 numpy库中的 …...

Flask-Migrate的使用
组织一个 Flask 项目通常需要遵循一定的结构,以便代码清晰、可维护。下面是一个典型的 Flask 项目结构: my_flask_app/ │ ├── app/ │ ├── __init__.py │ ├── models.py │ ├── views.py │ ├── forms.py │ ├── templat…...

python怎么输入整数
使用input()函数输入一个整数: ainput(“请输入一个整数\n”) 结果却报错TypeError: str object cannot be interpreted as an integer 查阅文档发现input()函数返回值是str型。 我们只需要这样转换: aint(input("请输入一…...

代码随想录打卡Day36
今天做的头皮发麻,除了第一道题是自己AC的,剩下两道题目都是看视频才AC的,主要是看了视频也花了很久时间才想清楚。 1049. 最后一块石头的重量 II 这道题一开始没什么思路,但是看到提示说和昨天的分割子集很像,然后我…...

速盾:凡科建站开cdn了吗?
凡科建站是一家专业的建站平台,提供了多种功能和工具来帮助用户快速搭建自己的网站。随着互联网技术的不断发展,网站的访问速度和稳定性成为了越来越重要的考虑因素。为了优化用户体验,提高网站的加载速度,凡科建站已经开启了CDN&…...

python贪吃蛇游戏项目源码【免费】
使用Pygame库实现的贪吃蛇游戏。Pygame是一个用于创建视频游戏的Python模块集合,它提供了图形和声音库,使游戏开发变得容易。 初始化设置 屏幕大小 (SCREEN_WIDTH, SCREEN_HEIGHT): 定义了游戏窗口的宽度和高度。方格大小 (SIZE): 定义了游戏中每个小方…...

Mycat搭建分库分表
分库分表解决的问题 单表数据量过大带来的性能和存储容量的限制的问题: 索引效率下降读写瓶颈存储容量限制事务性能问题分库分表架构 再搭建一对主从复制节点,3307主节点,3309从节点配置数据源 dw1 , dr1,创建集群c1创建逻辑库 CREATE DATAB…...

Python中的数据结构
1.列表 可以将列表理解为一个购物清单,这个清单里面的的每个元素可以是任何类型,并且可以重复(这里区别了列表与数组)。列表用“[]”表示。 #这里定义了一个列表,列表中的元素的类型不同。 lsit_a [a, b, c, 1, 2] …...

mysql笔记8(多表查询)
文章目录 1. union联合查询可能会用到去重操作 2. inner join 内连接3. left join 左连接4. right join 右连接5. cross join 交叉连接6. natural join 自然连接natural left join 自然左连接natural right join 自然右连接自然连接的两张表没有同名字段怎么办? 7. …...

typescript-tsconfig文件解释
ts.config.json 作用 用于标识 TypeScript 项目的根路径用于配置 TypeScript 编译器用于指定编译的文件 重要字段 files - 设置要编译的文件的名称;include - 设置需要进行编译的文件,支持路径模式匹配;exclude - 设置无需进行编译的文件…...

所有用贪心的算法和所有用动态规划(dp)的算法合集
贪心 纯贪心算法(常在普及组中做暴力题正解使用)DijkstraBFS(最短路径)KruskalPrimSollin(Boruvka)二分三分最值排序LCA(纯贪心版) 动态规划(dp) Floyd线性DP区间DP背…...
论文阅读 | 基于流模型和可逆噪声层的鲁棒水印框架(AAAI 2023)
Flow-based Robust Watermarking with Invertible Noise Layer for Black-box DistortionsAAAI, 2023,新加坡国立大学&中国科学技术大学本论文提出一种基于流的鲁棒数字水印框架,该框架采用了可逆噪声层来抵御黑盒失真。 一、问题 基于深度神经网络…...

上线跨境电商商城的步骤
上线一个跨境电商商城涉及多个步骤,从前期准备到上线后的维护。以下是一些关键步骤: 1. 市场调研与规划 目标市场分析:研究目标市场的需求、竞争对手和消费者行为。法律法规:了解并遵守目标市场的法律法规,包括税收、…...

Python基础(七)——PyEcharts数据分析(面向对象版)
四、使用PyEcharts数据分析案例(面向对象版) 【前言:为了巩固之前的Python基础知识(一)到(五),并为后续使用Python作为数据处理的好帮手,我们一起来用面向对象的思想来理…...

滚雪球学SpringCloud[5.1讲]: Spring Cloud Config详解
全文目录: 前言1. Spring Cloud Config的基本概念1.1 微服务配置管理的挑战1.2 Spring Cloud Config的架构 2. 配置服务端与客户端的配置2.1 配置服务端的搭建2.2 配置客户端的搭建2.3 环境隔离配置 3. 配置中心与Git的集成3.1 Git仓库的目录结构设计3.2 配置的版本…...

Unity常用随机数算法
Unity的Random.Range介绍 有两个重载: 如果参数存在至少一个浮点数那么将会触发public static float Range(float minInclusive, float maxInclusive); 返回一个范围内的浮点数(包含首尾) 如果参数都是整形则触发public static int Range(int minInclusive, int maxExclusiv…...

dial unix /var/run/docker.sock: connect: permission denied
要解决 permission denied 错误并授予当前用户 sunyuhua 访问 Docker 的权限,您可以按照以下步骤操作: 1. 检查 Docker 服务是否在运行 首先,确保 Docker 服务已经启动: sudo systemctl start docker sudo systemctl enable do…...

Prompt提示词技巧
文章目录 🍊 探索AI内容创作核心:提示词Prompt1 什么是提示词工程?1.1 提示词的原理1.2 提示词工程师的前景1.3 提示词工程师的门槛是否较低?1.4 提示词的未来展望 2 提示词编写的基本技巧3 常见的提示词框架3.1 CO-STAR框架3.2 BORKE框架3.…...

滑动窗口(6)_找到字符串中所有字母异位词
个人主页:C忠实粉丝 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C忠实粉丝 原创 滑动窗口(6)_找到字符串中所有字母异位词 收录于专栏【经典算法练习】 本专栏旨在分享学习算法的一点学习笔记,欢迎大家在评论区交流讨论Ǵ…...

【无标题】rocket
rocketMQ集群双主双从同步模式(2m-2s-sync)搭建-CSDN博客 集群架构概念 在部署的时候首先要将nameserver启动起来,之后就是将broker启动起来,broker启动起来会将自己的信息注册到nameserver上面。之后再去创建topic,因为发消息的逻辑和收消…...

Maven国内镜像(四种)
配置Maven使用国内镜像是一个常见的做法,因为这样可以显著提高依赖下载的速度并避免网络不稳定带来的问题 在 settings.xml 文件中,需要添加或修改 <mirrors> 标签来指定国内镜像。 以下是几个可用的镜像 1. 阿里云 <mirrors> <mi…...