首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >研发管理经验总结:2 致命漏洞!你的配置文件密码正在“裸奔”

研发管理经验总结:2 致命漏洞!你的配置文件密码正在“裸奔”

原创
作者头像
李福春
发布2025-07-05 00:06:17
发布2025-07-05 00:06:17
11700
代码可运行
举报
文章被收录于专栏:研发管理经验研发管理经验
运行总次数:0
代码可运行

1 故事-森林中的安全危机

在一片茂密的森林里,住着各种各样的动物。狐狸是这里的程序员,聪明机灵,负责编写和维护森林里的各种“程序”——也就是森林的运作规则。猫头鹰则是架构师,高瞻远瞩,负责设计整个森林的架构,确保一切井然有序。树懒是运维人员,虽然行动缓慢,但责任心强,负责日常的检查和维护。老虎则是这片森林的老板,威严而神秘。

有一天,狐狸在编写程序时,为了图方便,没有对配置文件中的密码进行加密。这一疏忽,让森林的安全隐患悄然滋生。不久后,一只好奇的小猴子发现了这个秘密,它偷偷记下了密码,并告诉了其他动物。很快,这个消息在森林里传开了,许多动物都开始尝试使用这些密码,导致森林的秩序大乱。

猫头鹰发现这个问题后,立刻向老虎汇报。老虎非常生气,责怪狐狸没有做好安全防护。狐狸深感愧疚,决定立即改正错误,对所有配置文件中的密码进行加密处理。经过一番努力,森林终于恢复了往日的宁静。这次事件也让所有动物意识到,安全无小事,任何一点疏忽都可能导致严重的后果。

2 需求场景

现状:当前软件系统中配置文件中的密码都是明文,如果泄露出去,会带来极大的风险。

a. 内网的中间件的配置和数据可能被修改;

b. 如果中间件是开放到公网的,代码如果泄露,中间件的数据和配置会被篡改。(如果是数据库层面,一定会被拖库,泄露数据);

目标:

1.配置文件中的密码加密,非开发团队成员看不到密码明文;

2.密码加密之后不能影响现有的功能,对应的中间件能正常连接,不需要修改程序;

3 解决路径

一、核心功能简介

jasypt-spring-boot-starter是 Spring Boot 的集成库,基于Jasypt(Java Simplified Encryption)实现配置文件敏感信息的加密与自动解密。主要功能包括:

  1. 自动解密:识别配置文件中的 ENC(密文)格式,在应用启动时自动解密。
  2. 算法支持:默认使用 PBEWithMD5AndDES,支持 AES-256等高安全性算法(需 JDK 1.9+)。
  3. 无缝集成:通过 Spring Boot 的自动配置机制,无需额外代码即可生效。

二、使用场景

适用于保护配置文件中的敏感信息,如:

  • 数据库密码、Redis 密码
  • API 密钥(如阿里云 SMS AccessKey)
  • 第三方服务的认证信息

核心价值:避免明文存储敏感数据,降低源码泄露或配置暴露的风险[。

综合对比,小团队,最快最简单的解决办法就是使用 jasypt去解决配置文件密码加密的问题;

4 解决过程

我们的工程是基于springboot的。

集成步骤:

a. 引入依赖

代码语言:xml
复制
<dependency>
    <groupId>com.github.ulisesbocchio</groupId>
    <artifactId>jasypt-spring-boot-starter</artifactId>
    <version>3.0.4</version>
</dependency>

b. 配置加密器

代码语言:javascript
代码运行次数:0
运行
复制
    @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提取出来,不同环境分开设置;

c. 内部运维用的加解密接口

对密码明文进行加密和解密

提供接口/页面给到运维人员使用,仅内网可以访问。

也可给一个对应的简易加解密页面。

代码语言:javascript
代码运行次数:0
运行
复制
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));    
    }
}

加密测试:

d. 把配置文件中的密码替换加密后的内容

注意密码的配置格式: ENC(密文)

如果不需要加密,则不带ENC(),使用明文即可;

e. 验证加密之后的功能

验证标准:

1.程序可以正常启动;

2.数据库能正常访问数据;

程序正常启动。并能正常的访问数据库。

5 运行原理

还得查看 starter的代码,从它的自动装配说起。

下面是源码跟踪过程,可能比较枯燥,但是却是精华部分。

自动装配的入口: META-INF/spring.factories

内容如下:

代码语言:javascript
代码运行次数:0
运行
复制
//springboot自动装配配置org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.ulisesbocchio.jasyptspringbootstarter.JasyptSpringBootAutoConfiguration
//springcloud启动配置org.springframework.cloud.bootstrap.BootstrapConfiguration=com.ulisesbocchio.jasyptspringbootstarter.JasyptSpringCloudBootstrapConfiguration

Plain Text

先从springboot看起。

代码:

代码语言:javascript
代码运行次数:0
运行
复制
//直接引入了一个配置文件的配置@Configuration@Import({EnableEncryptablePropertiesConfiguration.class})public class JasyptSpringBootAutoConfiguration {    public JasyptSpringBootAutoConfiguration() {    }}

Plain Text

springcloud的配置里面的代码也是一样的,引入了主角配置。但是提供了开关,即 jasypt.encryptor.bootstrap=false则不装配;

代码语言:javascript
代码运行次数:0
运行
复制
@Configuration@ConditionalOnProperty(    name = {"jasypt.encryptor.bootstrap"},    
havingValue = "true",    matchIfMissing = true)
@Import({EnableEncryptablePropertiesConfiguration.class})
public class JasyptSpringCloudBootstrapConfiguration {    
public JasyptSpringCloudBootstrapConfiguration() {    
}}

Plain Text

主角是: EnableEncryptablePropertiesConfiguration

代码语言:javascript
代码运行次数:0
运行
复制
@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 代码

代码语言:javascript
代码运行次数:0
运行
复制
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源码

源码太多了,我截取关键代码:

代码语言:javascript
代码运行次数:0
运行
复制
    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 的核心代码:

代码语言:javascript
代码运行次数:0
运行
复制
 @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

成功

使用手册

1 把你的密码明文进行加密

目前通过接口。

curl: http://localhost:8080/api/common/ops/password/encrypt/{你的密码明文}

得到密码的密文:

多次执行,密文不同,但是都能解密得到相同的明文。

2 把密文替换为原来的明文

在你的配置文件中替换。

举个例子:

原来的配置

password: Root1234

新的配置

password: ENC(A6jUVfzV5lhFtYKsPcJcPT6hlr90g45nj0dFyEW2VjMvHPlv)

替换则自动解密,不替换不影响,所以,不影响原有功能。

3 替换的范围跟踪

可以按照项目维度跟进。

业务相关

替换进度

责任人

mysql数据库

完成

carter

redis密码

完成

carter

cos密钥

完成

carter

企业微信密钥

完成

carter

小结

通过引入jasper的组件,完成了配置文件的密码的加密,减少了密码泄露的风险。

本文通过生动的森林寓言和详尽的技术解析,强调了配置文件密码加密的重要性。故事警示我们,即使是微小的疏忽也可能引发严重的安全危机。技术部分详细介绍了如何使用jasypt-spring-boot-starter实现密码加密,确保了配置文件的安全性,同时不影响系统的正常运行。文章不仅提供了实用的解决方案,还强调了安全意识的重要性,提醒我们在开发过程中始终保持警惕,防范潜在风险。

请给出你的回答:在现代软件开发中,你是如何对配置文件密码进行安全处理的?

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 故事-森林中的安全危机
  • 2 需求场景
  • 3 解决路径
    • 一、核心功能简介
    • 二、使用场景
  • 4 解决过程
    • a. 引入依赖
    • b. 配置加密器
    • c. 内部运维用的加解密接口
    • d. 把配置文件中的密码替换加密后的内容
    • e. 验证加密之后的功能
  • 5 运行原理
    • 封装
    • 使用手册
      • 1 把你的密码明文进行加密
      • 2 把密文替换为原来的明文
      • 3 替换的范围跟踪
  • 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档