一、先聊聊为什么要在意配置文件的安全

以前带过的一个项目,测试环境的数据库密码就写在application.properties里,结果有次代码打包发给了第三方公司,第二天测试库被删了。虽然不一定是密码泄露,但那个密码确实“裸奔”了。后来我们用Apollo统一管理配置,以为万事大吉——但Apollo后台能直接看到所有配置项,运维、测试、开发都有权限的话,数据库密码、第三方密钥等于还是一张白纸挂在墙上。给配置文件加密,本质上就是给这些敏感信息穿件“防弹衣”。

为什么强调“配置文件”而不是“配置中心”?因为Apollo只是一个存放和分发配置的“仓库”,它本身不会判断哪个配置敏感,也不会对内容加密。所有能访问Apollo后台的人,都能看到明文。只有我们自己在存储前对敏感值做加密处理,才能保证安全。

二、加密解密到底是怎么一回事

一句话,加密就是把“abc123”变成一长串乱码,解密就是拿着钥匙把乱码变回“abc123”。我们常说的对称加密,就是加密解密用同一把钥匙。常见的算法有AES、PBE等。你不需要自己去实现这些算法,直接用现成的库就行。我推荐用Jasypt,它和Spring Boot、Apollo都能配合得很好。用它的好处是,你只需要在配置里写“ENC(密文)”这种格式,项目启动时Jasypt会自动把“密文”替换成真正的明文。这个概念后面实战部分会再看到。

稍微多说一点:对称加密的“钥匙”就是密钥,同一个密钥既能加密也能解密。所以密钥千万不能丢,也不能让不该知道的人知道。那有人会问,为什么不用非对称加密?因为配置中心的场景大多是单向解密,对称加密性能好、实现简单,配合密钥管理服务完全够用。

三、动手给Apollo配置加密

技术栈:Java 8 + Spring Boot 2.7 + Apollo Client + Jasypt。这一章我们做一套完整的从依赖到使用的示例。

为什么选Jasypt而不是自己写AES工具类?Jasypt已经帮你处理了加密算法选择、密钥派生、密文格式转换等一堆细节,而且和Spring的Environment无缝衔接。自己写的话,很容易踩到“编码不一致”或“算法不兼容”的坑。等看完下面的步骤,你会发现原来省了很多事。

3.1 先把依赖加上

在pom.xml里加上Apollo客户端和Jasypt的依赖:

<dependency>
    <groupId>com.ctrip.framework.apollo</groupId>
    <artifactId>apollo-client</artifactId>
    <version>1.9.1</version>
</dependency>

<dependency>
    <groupId>com.github.ulisesbocchio</groupId>
    <artifactId>jasypt-spring-boot-starter</artifactId>
    <version>3.0.5</version>
</dependency>

第一段依赖用来连Apollo,第二段依赖就是给我们加密用的。

3.2 设置密钥和连接参数

在application.yml里配置Apollo和Jasypt。注意,密码不能写在这里,我从环境变量里取。

# application.yml
apollo:
  meta: http://你的apollo地址:8080 # Apollo meta server 地址
  bootstrap:
    enabled: true                   # 让Apollo配置在启动时加载
    eagerLoad:
      enabled: true                 # 越快加载越好

jasypt:
  encryptor:
    algorithm: PBEWITHHMACSHA512ANDAES_256  # 加密算法,安全性高
    password: ${JASYPT_ENCRYPTOR_PASSWORD}  # 密钥从环境变量读取
    prefix: "ENC("                   # 密文前缀
    suffix: ")"                      # 密文后缀

为什么密钥要放在环境变量?因为配置文件本身可能被提交到Git,放在环境变量里相当于把钥匙放在另一个保险柜,即使代码泄露,钥匙不会跟着泄露。

3.3 用一个小工具生成密文

我们写一个Java类,用Jasypt生成数据库密码的密文。注意,这个工具类只用来生成密文,不需要放进生产环境。

import org.jasypt.encryption.StringEncryptor;
import org.jasypt.encryption.pbe.PooledPBEStringEncryptor;
import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig;

public class EncryptorUtil {

    public static void main(String[] args) {
        // 假设密钥就是环境变量里的值,这里用默认的方便演示
        StringEncryptor encryptor = createEncryptor("mySecretKey");
        String plainText = "P@ssw0rd123"; // 要加密的数据库密码
        String cipherText = encryptor.encrypt(plainText);
        System.out.println("生成的密文:" + cipherText);
        // 输出类似: ENC(HfLmK2P8...)
    }

    /**
     * 创建加密器,配置和YAML里必须保持一致
     */
    private static StringEncryptor createEncryptor(String password) {
        PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
        SimpleStringPBEConfig config = new SimpleStringPBEConfig();
        config.setPassword(password);                         // 密钥
        config.setAlgorithm("PBEWITHHMACSHA512ANDAES_256");    // 算法
        config.setKeyObtentionIterations(1000);               // 迭代次数
        config.setPoolSize(1);                                // 池大小
        encryptor.setConfig(config);
        return encryptor;
    }
}

这里有个细节:算法的名字要和application.yml里一致,否则启动解密时会报错。

3.4 把密文放进Apollo配置中心

在Apollo后台新建一个配置项,比如键是“db.password”,值是上面生成的“ENC(...)”。也可能会添加一些非敏感配置。示例:

{
  "db.password": "ENC(HfLmK2P8xYQZ...)",
  "db.username": "root",
  "redis.token": "ENC(9zQm3nPv...)"
}

注意,这里只对敏感字段加密。像db.username这种不算太敏感的,可以明文存储,方便排查问题。

3.5 在代码里像原来一样使用

写一个Controller测试一下:

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ConfigController {

    @Value("${db.password}")   // 注入Apollo配置,自动解密
    private String dbPassword;

    @GetMapping("/get-password")
    public String getPassword() {
        // 这里拿到的已经是解密后的明文
        return dbPassword;
    }
}

运行项目,访问接口,就能看到真正的数据库密码。而Apollo后台和配置库里存的是密文。

四、解密机制是怎么工作的

Jasypt在Spring环境准备阶段会注册一个“解密器”,然后对所有从配置源读取的字符串进行规则匹配。如果发现以“ENC(”开头、以“)”结尾,就调用解密器。Apollo的配置会被Spring Environment管理,所以Jasypt也能处理。这个流程对代码透明,你写@Value时拿到的是明文。

可能有人好奇,这个过程是不是很慢?其实不会,配置项的数量不会特别大,解密一次也就在毫秒级别,而且Spring只会在启动时读取一次配置。除非你用@Value到动态刷新,否则解密开销可以忽略。

这里还有一个常见问题:为什么在Apollo后台看到的还是密文?因为Apollo只负责存储,不含解密功能。解密只发生在客户端运行时。这一点要和团队讲清楚,否则有人会以为配置已经加密了,但实际上后台明文仍然可见,只有值本身是密文。

另外,Jasypt还支持自定义前缀后缀,也支持不同的算法,灵活性很高。如果你不喜欢“ENC()”这种格式,也可以改成“SECRET[]”之类的,只要保持一致即可。

五、除了加密,还要注意这些安全点

加密很重要,但不能解决所有问题。

  1. 密钥管理:密钥必须放在环境变量、KMS或者专门的密钥管理服务中,千万别写进配置或代码。推荐使用云上的KMS(比如阿里云KMS、AWS KMS),这样轮换和权限控制都更好。
  2. 传输安全:Apollo配置中心最好启用HTTPS,防止中间人抓包获取配置密文。虽然密文没有密钥,但多一层保护总不是坏事。
  3. 权限控制:给不同环境、不同项目设置最小权限。Apollo支持命名空间级别的权限分配,记得用起来。比如开发人员只能看到测试环境的配置,生产环境只有运维能改。
  4. 审计日志:谁在后台改了什么配置,应该留痕。Apollo自带审计,要开启。尤其是敏感配置的变更记录,方便事后排查。
  5. 内存安全:解密后的明文会留在JVM内存中,如果有人能dump内存,一样能拿到。所以关键操作后要及时清理,或者使用更高级的安全方案。

六、这种方案的好处和麻烦

好处很直接:

  • 配置中心里的数据即使泄露,没有密钥也解不开。
  • 开发人员日常不需要知道数据库密码,一定程度上减小泄密面。
  • 满足很多安全合规要求,比如等保测评里常常要求敏感配置加密存储。

麻烦也不少:

  • 密钥管理需要额外运维,比如轮换、分发。
  • 加解密会消耗一点点性能,不过对于配置读取这种低频操作可以忽略。
  • 在本地调试时,如果没配置好环境变量,程序就起不来。
  • 加密粒度不好控制,加密太多影响排查问题。

举个例子,之前同事把整个application.yml都加密了,结果日志里打印配置全是乱码,排查线上问题时很痛苦。所以还是要按需加密。

七、避开这些坑

  • 别把密钥写在application.yml里,那和没加密一样。
  • 别用弱算法(比如DES),推荐用AES-256。
  • 密钥轮换时要注意动态刷新,Apollo配合Spring Cloud Bus可以实现动态更新,但Jasypt的密钥本身不能直接热更新,这是一个难点。通常做法是重启应用,或者结合自定义加密器。
  • 密文不可逆,一旦密钥丢了,密文就作废。所以密钥要有备份机制。
  • 不要对全部配置加密,只加密真正敏感的信息。
  • 注意密文可能包含特殊字符,在Apollo编辑时要小心不要被格式化工具转义。
  • 如果你使用Spring Cloud Gateway或者以Bootstrap方式启动项目,注意Jasypt的初始化顺序,确保它在读取配置之前就注册好。否则可能遇到解密失败。

八、总结

Apollo让配置管理变得集中,但要真正安全,还需要在配置文件这一层加把锁。用Jasypt加密敏感配置,加上密钥管理和权限控制,才能算是一个相对完整的方案。安全没有终点,永远得多想一步。希望这篇啰嗦的内容,能帮你在实际项目中少踩几个配置泄露的坑。