一、先聊聊为什么要在意配置文件的安全
以前带过的一个项目,测试环境的数据库密码就写在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[]”之类的,只要保持一致即可。
五、除了加密,还要注意这些安全点
加密很重要,但不能解决所有问题。
- 密钥管理:密钥必须放在环境变量、KMS或者专门的密钥管理服务中,千万别写进配置或代码。推荐使用云上的KMS(比如阿里云KMS、AWS KMS),这样轮换和权限控制都更好。
- 传输安全:Apollo配置中心最好启用HTTPS,防止中间人抓包获取配置密文。虽然密文没有密钥,但多一层保护总不是坏事。
- 权限控制:给不同环境、不同项目设置最小权限。Apollo支持命名空间级别的权限分配,记得用起来。比如开发人员只能看到测试环境的配置,生产环境只有运维能改。
- 审计日志:谁在后台改了什么配置,应该留痕。Apollo自带审计,要开启。尤其是敏感配置的变更记录,方便事后排查。
- 内存安全:解密后的明文会留在JVM内存中,如果有人能dump内存,一样能拿到。所以关键操作后要及时清理,或者使用更高级的安全方案。
六、这种方案的好处和麻烦
好处很直接:
- 配置中心里的数据即使泄露,没有密钥也解不开。
- 开发人员日常不需要知道数据库密码,一定程度上减小泄密面。
- 满足很多安全合规要求,比如等保测评里常常要求敏感配置加密存储。
麻烦也不少:
- 密钥管理需要额外运维,比如轮换、分发。
- 加解密会消耗一点点性能,不过对于配置读取这种低频操作可以忽略。
- 在本地调试时,如果没配置好环境变量,程序就起不来。
- 加密粒度不好控制,加密太多影响排查问题。
举个例子,之前同事把整个application.yml都加密了,结果日志里打印配置全是乱码,排查线上问题时很痛苦。所以还是要按需加密。
七、避开这些坑
- 别把密钥写在application.yml里,那和没加密一样。
- 别用弱算法(比如DES),推荐用AES-256。
- 密钥轮换时要注意动态刷新,Apollo配合Spring Cloud Bus可以实现动态更新,但Jasypt的密钥本身不能直接热更新,这是一个难点。通常做法是重启应用,或者结合自定义加密器。
- 密文不可逆,一旦密钥丢了,密文就作废。所以密钥要有备份机制。
- 不要对全部配置加密,只加密真正敏感的信息。
- 注意密文可能包含特殊字符,在Apollo编辑时要小心不要被格式化工具转义。
- 如果你使用Spring Cloud Gateway或者以Bootstrap方式启动项目,注意Jasypt的初始化顺序,确保它在读取配置之前就注册好。否则可能遇到解密失败。
八、总结
Apollo让配置管理变得集中,但要真正安全,还需要在配置文件这一层加把锁。用Jasypt加密敏感配置,加上密钥管理和权限控制,才能算是一个相对完整的方案。安全没有终点,永远得多想一步。希望这篇啰嗦的内容,能帮你在实际项目中少踩几个配置泄露的坑。
Comments