DISCONF 配置数据加密和自动解密

Disconf 加密配置自动解密

为啥要做这件事

Disconf 里经常放 AK、SK、API Key 这类东西。以前是明文,改配置方便,可配置中心能看的人不少,仓库、下载目录里也能碰到。

最近给 Disconf 配了自动解密:敏感配置在控制台里存密文,应用启动时自动解开,再注入到 @Value 字段。业务代码不用每个字段自己解密,改配置也还是热更新那套。

目标就三件事:

  • Disconf 里只存密文
  • 业务代码还是@Value("${xxx}"),不用每个字段自己解密
  • 启动能解,热更新也能解,行为一致

下面把接入步骤、核心知识点,以及上线后踩到的坑讲清楚。

先说结论

Disconf 托管配置走的是 Spring 的PropertyPlaceholderConfigurer这一套。启动时 Spring 会先 convertProperties,再解析 ${},所以重写convertPropertyValue就能解密。

热更新不是这条路。Disconf 会直接拿 mergeProperties() 的返回值去改 Bean,不会再走一遍 convertProperties。密文就这么被塞进字段了。

所以自定义 Configurer 里两件事都要做:

1.convertPropertyValue:识别密文并解密
2.mergeProperties:合并完立刻 convertProperties,让热更新也吃到明文
约定很简单:密文后面跟 _encrypted。不是这个后缀,原样返回。

接入怎么做

1. XML 里把 Configurer 换成自己的

原来用的是 Disconf 自带的 ReloadingPropertyPlaceholderConfigurer。它负责两件事:启动时解析占位符,配置文件变了再把新值写回 Bean。

自动解密就插在这一层。业务 Bean 不用动。

<bean id="application_Properties"

      class="com.baidu.disconf.client.addons.properties.ReloadablePropertiesFactoryBean">

    <property name="locationPaths">

        <list>

            <value>classpath:*/xxx1.properties</value>

            <value>classpath:*/xxx2.properties</value>

            <!-- 其他托管文件 -->

        </list>

    </property>

</bean>


<bean id="applicationPropertyConfigurer"

      class="weli.spring.config.ReloadingEncryptedPropertyPlaceholderConfigurer">

    <property name="ignoreUnresolvablePlaceholders" value="true"/>

    <property name="propertiesArray">

        <list>

            <ref bean="application_Properties"/>

        </list>

    </property>

</bean>  

敏感配置单独放到  xxx.properties  这类文件里,Disconf 上托管的是加密版本。普通开关、白名单还走原来的明文文件就行。

2. 自定义 ReloadingEncryptedPropertyPlaceholderConfigurer

核心就两个方法:

public class ReloadingEncryptedPropertyPlaceholderConfigurer

        extends ReloadingPropertyPlaceholderConfigurer {


    private static final String ENCRYPTED_SUFFIX = "_encrypted";


    /**

     * Disconf 热更新会直接用 mergeProperties() 的返回值更新 Bean,

     * 不会走 Spring 启动期的 convertProperties。

     * 合并完立刻解密,热更新才跟启动一致。

     */

    @Override

    protected Properties mergeProperties() throws IOException {

        Properties properties = super.mergeProperties();

        convertProperties(properties);

        return properties;

    }


    @Override

    protected String convertPropertyValue(String originalValue) {

        if (!StringUtils.endsWith(originalValue, ENCRYPTED_SUFFIX)) {

            return originalValue;

        }

        String cipherText = originalValue.substring(

                0, originalValue.length() - ENCRYPTED_SUFFIX.length());

        return KmsAesUtils.decrypt(cipherText, resolveEncryptKey());

    }

}

业务侧不用改。 CommonDisconfConfigService这一类java类还是:

c@Value("${volc.ai.apiKey}")  
private String volcApiKey;  

字段拿到的已经是明文。

3. Disconf 里怎么配

加密值按这个格式:

{Base64密文}_encrypted

例如:

# 明文时代
volc.ai.apiKey=sk-xxxxxxxx

# 加密后
volc.ai.apiKey=AbCdEf....==_encrypted

识别靠后缀,不靠 key 名字。同一个文件里,明文和密文可以混着放。开关继续写1 / 0,密钥才加 _encrypted

解密走统一的 KMS AES 工具,密钥不要写进博客、不要进 Git 明文。生产上从 KMS 或受控配置拿。

上线后遇到的问题:启动能解,热更新为啥不解?

接入本身不复杂,自定义一个 Configurer,重写 convertPropertyValue就行。按 Spring 的扩展方式接入后,启动、重启验证都正常:@Value拿到的是明文,下游 SDK 也能调通。

上线后有同事在 Disconf 里改了一个加密配置。热更新生效了,Bean 字段却变成了 xxxx_encrypted这种密文。下游拿着密文去调第三方,直接失败。

重启服务又恢复正常。也就是说:只重启永远看不出问题,一改配置就把密文写进内存。

不是解密逻辑写错了,是热更新根本没走进解密。

为啥会这样

Spring 的 PropertyPlaceholderConfigurer 继承自 PropertyResourceConfigurer。启动时大概是这样:

postProcessBeanFactory  
  ├─ mergeProperties()        // 把本地文件、Disconf 下载文件合成一份 Properties
  ├─ convertProperties()      // 遍历每个 value,调用 convertPropertyValue
  └─ processProperties()      // 解析 ${},注入 Bean

convertProperties会把每个 value 丢给 convertPropertyValue。启动期重写这个方法,密文在占位符解析之前就已经变成明文了。所以重启验证会过。

问题在热更新。Disconf 的 ReloadingPropertyPlaceholderConfigurer 实现了 IReloadablePropertiesListener。配置文件一变,走的是propertiesReloaded

配置文件变更
└─ propertiesReloaded()
    ├─ 取出 lastMergedProperties(旧值)
    ├─ 再调一次 mergeProperties()(新值,原始文本)
    ├─ 对比哪些 key 变了
    ├─ parseStringValue(占位符, 新 Properties)   // 直接从 Properties 取值
    └─ BeanWrapper.setPropertyValue(...)         // 写回字段

注意中间没有convertProperties

parseStringValue就是按 key 从这份 Properties 里取值。Properties 里还是 xxxx_encrypted,字段就被写成密文了。

怎么解决的

热更新能不能解密,不取决于有没有重写 convertPropertyValue,而取决于送进 parseStringValue 的那份 Properties 是不是已经解密。

启动和热更新都会走到 mergeProperties,这是两条链路的交汇点。于是在合并完成后主动调一次 convertProperties

@Override
protected Properties mergeProperties() throws IOException {  
    Properties properties = super.mergeProperties();
    convertProperties(properties);
    return properties;
}

这样启动和热更新共用已经解密的Properties。convertPropertyValue 仍然只负责“看到_encrypted就解密”,职责没有变。

Disconf 自己的mergeProperties还会把结果缓存到 lastMergedProperties。我们是在 super.mergeProperties() 之后原地 convertProperties,缓存里也是明文。下次对比旧值、新值,比的都是解密后的内容,不会把“密文变密文”误判成没变化。

启动时 convertProperties其实会跑两遍:我们在mergeProperties 里跑一次,Spring 的postProcessBeanFactory再跑一次。没关系。解密过的值已经没有_encrypted后缀,第二次原样返回。

两条链路,一张图看完 mergeProperties的原因就一句话:把解密提前到“Properties 刚合并完”,启动和热更新共用这一份已经解密的 Properties。

核心知识点

1. Spring 预留了“改 value”的钩子,但只保证启动期

PropertyResourceConfigurer 里大概是这样:

protected void convertProperties(Properties props) {
Enumeration<?> names = props.propertyNames();
while (names.hasMoreElements()) {
    String name = (String) names.nextElement();
    String value = props.getProperty(name);
    String converted = convertProperty(name, value);
    if (!ObjectUtils.nullSafeEquals(value, converted)) {
        props.setProperty(name, converted);
    }
}
}

protected String convertPropertyValue(String originalValue) {
return originalValue; // 默认啥也不干
}

这是给加密、脱敏、统一裁剪留的扩展点。Jasypt 的 EncryptablePropertyPlaceholderConfigurer 也是走这条路。前提是调用方会走 postProcessBeanFactory。 Disconf 热更新自己另起了一条路,这个钩子就用不上了。

2. Disconf 热更新是“直接改对象”,不是“重新走一遍 Spring 注入”

启动时 @Value靠 BeanFactory 后置处理。热更新时 Bean 已经在容器里了,Disconf 用 BeanWrapper.setPropertyValue按属性名改字段。

所以热更新能不能解密,不取决于有没有重写 convertPropertyValue,而取决于送进 parseStringValue 的那份 Properties 是不是已经解密。

3. 用后缀标记密文,比改 key 名更合适

没有用 ENC(...)这种包裹,也没有搞 xxx.encrypted=true这种并列 key,就是 value 末尾加 _encrypted

好处:

  • Disconf 控制台里一眼能看出来哪些是密文
  • 明文、密文可以混在同一个 properties 里
  • @Value("${volc.ai.apiKey}")不用改
  • 解密过的值没有后缀, convertPropertyValue再进来也是幂等的,不用怕跑两遍 代价:约定要守住。明文别随手加 _encrypted,不然会拿去当密文解,直接失败。
4. mergeProperties 才是两条链路的交汇点

要让行为一致,解密放在交汇点最合适。只改 convertPropertyValue,等于只覆盖了表格第一行。

5.托管文件和注解注入是配套的

项目里 Disconf 有两种用法:

  • XML 托管 properties / yml,靠 PlaceholderConfigurer 解析 ${},再注入 @Value
  • @DisconfFile这类 Java 配置类

这次解密挂在 PlaceholderConfigurer 上,覆盖的是第一种。走托管文件 + @Value的密钥都能自动解。如果以后有配置类自己读文件内容,那是另一条链路,不会经过这个 Configurer。

使用时注意几点


加密范围别铺太大。 开关、名单、URL 没必要加密,增加排障成本。AK、SK、Token、密钥才加密。


热更新要专门测一次。 只重启验证会漏。改 Disconf 里的加密值,看 Bean 字段是不是明文、下游调用是不是正常。日志里不要把解密后的密钥打出来。


解密失败要让它暴露。 别吞异常再返回原文。密文解不开,宁可启动失败或热更新失败,也好过带着错误密钥跑到第三方。


密钥本身别再进 Disconf 明文。 配置解密的 key 如果也明文放 Disconf,等于锁和钥匙放一起。走 KMS 或机器侧受控配置。


后缀是契约。 加密工具吐出来的字符串,末尾必须是_encrypted,和接口响应加密那套标记可以保持一致,运营、排障都好认。

小结

这次接入可以压成三句话:

1.把 Disconf 的 ReloadingPropertyPlaceholderConfigurer换成带解密的子类,业务 @Value不用动。
2.convertPropertyValue 负责“看到_encrypted就解密”,解决启动期。
3.mergeProperties里主动 convertProperties,解决热更新不走 Spring 转换钩子的问题。
看起来像多写了十来行。真正有用的是分清两条链路:启动走 Spring 的 convertProperties,热更新走 Disconf 的 mergeProperties+ BeanWrapper。钩子挂错地方,重启永远正常,一改配置就把密文写进内存。

作者介绍:

  • 刘杰 资深服务端开发工程师

微鲤技术团队

微鲤技术团队承担了中华万年历、Maybe、蘑菇语音、微鲤游戏高达3亿用户的产品研发工作,并构建了完备的大数据平台、基础研发框架、基础运维设施。践行数据驱动理念,相信技术改变世界。