在一片茂密的森林里,住着各种各样的动物。狐狸是这里的程序员,聪明机灵,负责编写和维护森林里的各种“程序”——也就是森林的运作规则。猫头鹰则是架构师,高瞻远瞩,负责设计整个森林的架构,确保一切井然有序。树懒是运维人员,虽然行动缓慢,但责任心强,负责日常的检查和维护。老虎则是这片森林的老板,威严而神秘。
有一天,狐狸在编写程序时,为了图方便,没有对配置文件中的密码进行加密。这一疏忽,让森林的安全隐患悄然滋生。不久后,一只好奇的小猴子发现了这个秘密,它偷偷记下了密码,并告诉了其他动物。很快,这个消息在森林里传开了,许多动物都开始尝试使用这些密码,导致森林的秩序大乱。
猫头鹰发现这个问题后,立刻向老虎汇报。老虎非常生气,责怪狐狸没有做好安全防护。狐狸深感愧疚,决定立即改正错误,对所有配置文件中的密码进行加密处理。经过一番努力,森林终于恢复了往日的宁静。这次事件也让所有动物意识到,安全无小事,任何一点疏忽都可能导致严重的后果。
现状:当前软件系统中配置文件中的密码都是明文,如果泄露出去,会带来极大的风险。
a. 内网的中间件的配置和数据可能被修改;
b. 如果中间件是开放到公网的,代码如果泄露,中间件的数据和配置会被篡改。(如果是数据库层面,一定会被拖库,泄露数据);
目标:
1.配置文件中的密码加密,非开发团队成员看不到密码明文;
2.密码加密之后不能影响现有的功能,对应的中间件能正常连接,不需要修改程序;
jasypt-spring-boot-starter
是 Spring Boot 的集成库,基于Jasypt(Java Simplified Encryption)实现配置文件敏感信息的加密与自动解密。主要功能包括:
ENC(密文)
格式,在应用启动时自动解密。PBEWithMD5AndDES
,支持 AES-256
等高安全性算法(需 JDK 1.9+)。适用于保护配置文件中的敏感信息,如:
核心价值:避免明文存储敏感数据,降低源码泄露或配置暴露的风险[。
综合对比,小团队,最快最简单的解决办法就是使用 jasypt去解决配置文件密码加密的问题;
我们的工程是基于springboot的。
集成步骤:
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.4</version>
</dependency>
@Bean("jasyptStringEncryptor")
public StringEncryptor stringEncryptor() {
SimpleGCMConfig config = new SimpleGCMConfig();
//设置加密密钥 config.setSecretKeyPassword("xxxxx9578");
config.setSecretKeyIterations(1000);
//标准的base64编码,否则会报错,可以在线生成一个字符串的在线编码
config.setSecretKeySalt("TG92ZV9DYXJ0ZXJfMjAyNQ==");
config.setSecretKeyAlgorithm("PBKDF2WithHmacSHA256");
log.info("安装属性配置文件密码增强成功!");
return new SimpleGCMStringEncryptor(config);
}
也可以自定义配置属性,把SecretKeyPassword提取出来,不同环境分开设置;
对密码明文进行加密和解密
提供接口/页面给到运维人员使用,仅内网可以访问。
也可给一个对应的简易加解密页面。
package com.joysky.joycodeapi.common.rest.ops;
import com.joysky.joycode.api.lib.common.bean.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.jasypt.encryption.StringEncryptor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/** * @description: 密码加密解密控制器,提供给运维加密和解密用
* @see:com.joysky.joycode.api.common.rest.ops
* @author:carter * @createTime:2022/2/11 10:54 */
@Api(tags = "运维安全接口")
@RestController
public class PasswordController {
private final StringEncryptor jasyptStringEncryptor;
public PasswordController(StringEncryptor jasyptStringEncryptor) {
this.jasyptStringEncryptor = jasyptStringEncryptor;
}
@ApiOperation("加密接口") @GetMapping("/ops/password/encrypt/{password}")
public R<String> encrypt(@PathVariable("password")String password){
return R.ok(jasyptStringEncryptor.encrypt(password));
}
@ApiOperation("解密接口") @GetMapping("/ops/password/decrypt/{password}")
public R<String> decrypt(@PathVariable("password")String password){
return R.ok(jasyptStringEncryptor.decrypt(password));
}
}
加密测试:
注意密码的配置格式: ENC(密文)
如果不需要加密,则不带ENC(),使用明文即可;
验证标准:
1.程序可以正常启动;
2.数据库能正常访问数据;
程序正常启动。并能正常的访问数据库。
还得查看 starter的代码,从它的自动装配说起。
下面是源码跟踪过程,可能比较枯燥,但是却是精华部分。
自动装配的入口: META-INF/spring.factories
内容如下:
//springboot自动装配配置org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.ulisesbocchio.jasyptspringbootstarter.JasyptSpringBootAutoConfiguration
//springcloud启动配置org.springframework.cloud.bootstrap.BootstrapConfiguration=com.ulisesbocchio.jasyptspringbootstarter.JasyptSpringCloudBootstrapConfiguration
Plain Text
先从springboot看起。
代码:
//直接引入了一个配置文件的配置@Configuration@Import({EnableEncryptablePropertiesConfiguration.class})public class JasyptSpringBootAutoConfiguration { public JasyptSpringBootAutoConfiguration() { }}
Plain Text
springcloud的配置里面的代码也是一样的,引入了主角配置。但是提供了开关,即 jasypt.encryptor.bootstrap=false则不装配;
@Configuration@ConditionalOnProperty( name = {"jasypt.encryptor.bootstrap"},
havingValue = "true", matchIfMissing = true)
@Import({EnableEncryptablePropertiesConfiguration.class})
public class JasyptSpringCloudBootstrapConfiguration {
public JasyptSpringCloudBootstrapConfiguration() {
}}
Plain Text
主角是: EnableEncryptablePropertiesConfiguration
@Configuration
@Import({EncryptablePropertyResolverConfiguration.class, CachingConfiguration.class})
@Slf4j
public class EnableEncryptablePropertiesConfiguration {
@Bean
public static EnableEncryptablePropertiesBeanFactoryPostProcessor enableEncryptablePropertySourcesPostProcessor(final ConfigurableEnvironment environment, EncryptablePropertySourceConverter converter) {
return new EnableEncryptablePropertiesBeanFactoryPostProcessor(environment, converter);
}}
Plain Text
注释:内容我翻译一下。
配置文件注册了一个 BeanFactoryPostProcessor , 包装了所有的 在环境中Environment 的 PropertySource ,通过类: EncryptablePropertySourceWrapper
定义了一个默认的StringEncryptor用来加密配置, 可以通过配置的方式。 默认的 StringEncryptor会创建当应用上下文 ApplicationContext中 没有定义的时候
默认的StringEncryptor 属性见下表。
Key | 是否必填 | 默认值 |
---|---|---|
jasypt.encryptor.password | True | - |
jasypt.encryptor.algorithm | False | PBEWITHHMACSHA512ANDAES_256 |
jasypt.encryptor.keyObtentionIterations | False | 1000 |
jasypt.encryptor.poolSize | False | 1 |
jasypt.encryptor.providerName | False | SunJCE |
jasypt.encryptor.saltGeneratorClassname | False | org.jasypt.salt.RandomSaltGenerator |
jasypt.encryptor.ivGeneratorClassname | False | org.jasypt.iv.RandomIvGenerator |
jasypt.encryptor.stringOutputType | False | base64 |
接下来定义了一个 EnableEncryptablePropertiesBeanFactoryPostProcessor
,注入了一个类: EncryptablePropertySourceConverter
引入了: EncryptablePropertyResolverConfiguration
CachingConfiguration
下面分别看一下这4个类分别干了啥。
EnableEncryptablePropertiesBeanFactoryPostProcessor 代码
public class EnableEncryptablePropertiesBeanFactoryPostProcessor
implements BeanFactoryPostProcessor, Ordered {
private final ConfigurableEnvironment environment;
private final EncryptablePropertySourceConverter converter;
public EnableEncryptablePropertiesBeanFactoryPostProcessor(ConfigurableEnvironment environment, EncryptablePropertySourceConverter converter) { this.environment = environment; this.converter = converter; }
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
log.info("Post-processing PropertySource instances");
MutablePropertySources propSources = environment.getPropertySources();
converter.convertPropertySources(propSources);
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 100;
}}
主要干的事情: 从ConfigurableEnvironment中拿到所有的配置,然后使用转换器进行转换;
大家都知道,BeanFactoryPostProcessor 是一个前置增强,所有在spring的上下文中的Bean都能拦截到进行处理。
EncryptablePropertySourceConverter源码
源码太多了,我截取关键代码:
public void convertPropertySources(MutablePropertySources propSources) {
propSources.stream()
.filter(ps -> !(ps instanceof EncryptablePropertySource))
.map(this::makeEncryptable)
.collect(toList())
.forEach(ps -> propSources.replace(ps.getName(), ps));
}
public <T> PropertySource<T> makeEncryptable(PropertySource<T> propertySource) {
if (propertySource instanceof EncryptablePropertySource || skipPropertySourceClasses.stream().
anyMatch(skipClass -> skipClass.equals(propertySource.getClass()))) {
if (!(propertySource instanceof EncryptablePropertySource)) {
log.info("Skipping PropertySource {} [{}", propertySource.getName(), propertySource.getClass()); } return propertySource; } PropertySource<T> encryptablePropertySource = convertPropertySource(propertySource); return encryptablePropertySource; }
private <T> PropertySource<T> convertPropertySource(PropertySource<T> propertySource) {
return interceptionMode == InterceptionMode.PROXY ?
proxyPropertySource(propertySource) : instantiatePropertySource(propertySource); }
private <T> PropertySource<T> instantiatePropertySource(PropertySource<T> propertySource) {
PropertySource<T> encryptablePropertySource;
if (needsProxyAnyway(propertySource)) {
encryptablePropertySource = proxyPropertySource(propertySource); }
else if (propertySource instanceof SystemEnvironmentPropertySource) {
encryptablePropertySource = (PropertySource<T>)
new EncryptableSystemEnvironmentPropertySourceWrapper((SystemEnvironmentPropertySource)
propertySource, propertyResolver, propertyFilter); }
else if (propertySource instanceof MapPropertySource) {
encryptablePropertySource = (PropertySource<T>)
new EncryptableMapPropertySourceWrapper((MapPropertySource) propertySource,
propertyResolver, propertyFilter); }
else if (propertySource instanceof EnumerablePropertySource) {
encryptablePropertySource = new EncryptableEnumerablePropertySourceWrapper<>((
EnumerablePropertySource) propertySource, propertyResolver, propertyFilter);
} else { encryptablePropertySource =
new EncryptablePropertySourceWrapper<>(propertySource, propertyResolver,
propertyFilter); } return encryptablePropertySource;
}
Plain Text
流程如下:
再来看看 EncryptablePropertyResolverConfiguration
的核心代码:
@SuppressWarnings("unchecked") @Bean public static EncryptablePropertySourceConverter encryptablePropertySourceConverter(ConfigurableEnvironment environment, @Qualifier(RESOLVER_BEAN_NAME) EncryptablePropertyResolver propertyResolver, @Qualifier(FILTER_BEAN_NAME) EncryptablePropertyFilter propertyFilter) { final boolean proxyPropertySources = environment.getProperty("jasypt.encryptor.proxy-property-sources", Boolean.TYPE, false); final List<String> skipPropertySources = (List<String>) environment.getProperty("jasypt.encryptor.skip-property-sources", List.class, Collections.EMPTY_LIST); final List<Class<PropertySource<?>>> skipPropertySourceClasses = skipPropertySources.stream().map(EncryptablePropertySourceConverter::getPropertiesClass).collect(Collectors.toList()); final InterceptionMode interceptionMode = proxyPropertySources ? InterceptionMode.PROXY : InterceptionMode.WRAPPER; return new EncryptablePropertySourceConverter(interceptionMode, skipPropertySourceClasses, propertyResolver, propertyFilter); }
@SuppressWarnings("unchecked") @Bean public EnvCopy envCopy(final ConfigurableEnvironment environment) { return new EnvCopy(environment); }
@Bean(name = ENCRYPTOR_BEAN_NAME) public StringEncryptor stringEncryptor( final EnvCopy envCopy, final BeanFactory bf) { final String customEncryptorBeanName = envCopy.get().resolveRequiredPlaceholders(ENCRYPTOR_BEAN_PLACEHOLDER); final boolean isCustom = envCopy.get().containsProperty(ENCRYPTOR_BEAN_PROPERTY); return new DefaultLazyEncryptor(envCopy.get(), customEncryptorBeanName, isCustom, bf); }
@Bean(name = DETECTOR_BEAN_NAME) public EncryptablePropertyDetector encryptablePropertyDetector( final EnvCopy envCopy, final BeanFactory bf) { final String customDetectorBeanName = envCopy.get().resolveRequiredPlaceholders(DETECTOR_BEAN_PLACEHOLDER); final boolean isCustom = envCopy.get().containsProperty(DETECTOR_BEAN_PROPERTY); return new DefaultLazyPropertyDetector(envCopy.get(), customDetectorBeanName, isCustom, bf); }
@Bean(name = CONFIG_SINGLETON) public Singleton<JasyptEncryptorConfigurationProperties> configProps( final EnvCopy envCopy) { return new Singleton<>(() -> JasyptEncryptorConfigurationProperties.bindConfigProps(envCopy.get())); }
//自定义配置文件过滤器 @Bean(name = FILTER_BEAN_NAME) public EncryptablePropertyFilter encryptablePropertyFilter( final EnvCopy envCopy, final ConfigurableBeanFactory bf) { final String customFilterBeanName = envCopy.get().resolveRequiredPlaceholders(FILTER_BEAN_PLACEHOLDER); final boolean isCustom = envCopy.get().containsProperty(FILTER_BEAN_PROPERTY); return new DefaultLazyPropertyFilter(envCopy.get(), customFilterBeanName, isCustom, bf); }
@Bean(name = RESOLVER_BEAN_NAME) public EncryptablePropertyResolver encryptablePropertyResolver( @Qualifier(DETECTOR_BEAN_NAME) final EncryptablePropertyDetector propertyDetector, @Qualifier(ENCRYPTOR_BEAN_NAME) final StringEncryptor encryptor, final BeanFactory bf, final EnvCopy envCopy, final ConfigurableEnvironment environment) { final String customResolverBeanName = envCopy.get().resolveRequiredPlaceholders(RESOLVER_BEAN_PLACEHOLDER); final boolean isCustom = envCopy.get().containsProperty(RESOLVER_BEAN_PROPERTY); return new DefaultLazyPropertyResolver(propertyDetector, encryptor, customResolverBeanName, isCustom, bf, environment); }
JasyptEncryptorConfigurationProperties 这个是starter对外暴露的配置文件。 前缀是: jasypt.encryptor
代码比较长,后面再细跟。 先封装使用。后面结合实际问题去分析更高效。
封装到我们的基础框架里面。
放到 apilib-rest中;具体看代码;
去掉测试工程中的配置。
测试4个工程;
工程 | 运行情况 |
---|---|
common | 成功 |
devops | 成功 |
apprun | 成功 |
目前通过接口。
curl: http://localhost:8080/api/common/ops/password/encrypt/{你的密码明文}
得到密码的密文:
多次执行,密文不同,但是都能解密得到相同的明文。
在你的配置文件中替换。
举个例子:
原来的配置 | password: Root1234 |
---|---|
新的配置 | password: ENC(A6jUVfzV5lhFtYKsPcJcPT6hlr90g45nj0dFyEW2VjMvHPlv) |
替换则自动解密,不替换不影响,所以,不影响原有功能。
可以按照项目维度跟进。
业务相关 | 替换进度 | 责任人 |
---|---|---|
mysql数据库 | 完成 | carter |
redis密码 | 完成 | carter |
cos密钥 | 完成 | carter |
企业微信密钥 | 完成 | carter |
通过引入jasper的组件,完成了配置文件的密码的加密,减少了密码泄露的风险。
本文通过生动的森林寓言和详尽的技术解析,强调了配置文件密码加密的重要性。故事警示我们,即使是微小的疏忽也可能引发严重的安全危机。技术部分详细介绍了如何使用jasypt-spring-boot-starter实现密码加密,确保了配置文件的安全性,同时不影响系统的正常运行。文章不仅提供了实用的解决方案,还强调了安全意识的重要性,提醒我们在开发过程中始终保持警惕,防范潜在风险。
请给出你的回答:在现代软件开发中,你是如何对配置文件密码进行安全处理的?
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。