在开发Spring Boot应用时,配置文件里经常需要存放数据库密码、API密钥等敏感信息。这些信息如果以明文形式存在,一旦配置文件泄露,就会造成严重的安全问题。想象一下,如果有人拿到了你的数据库密码,就能直接访问你的生产数据库,这简直是一场灾难。
我遇到过不少团队直接把密码写在配置文件里,甚至把这些文件提交到了Git仓库。后来他们不得不紧急修改密码,重新部署所有服务。更糟糕的是,有些密码可能已经被恶意利用,造成了数据泄露。这就是为什么我们需要对敏感配置进行加密。
Jasypt提供了一种优雅的解决方案:它允许我们将加密后的字符串放在配置文件中,运行时自动解密。这样即使配置文件泄露,攻击者也无法直接获取原始密码。加密后的字符串看起来像这样:ENC(tgjLqtpTOf7z6IC+xE9r+w==),只有知道密钥的人才能解密出原始内容。
Spring Boot的核心魔法在于它的环境抽象,所有的配置都通过PropertySource体系来管理。当我们需要读取一个配置值时,Spring会依次检查各个PropertySource,直到找到对应的值。Jasypt正是利用了这个机制,在配置读取的过程中插入了解密逻辑。
具体来说,Jasypt通过一个BeanFactoryPostProcessor,在Spring容器初始化阶段,把所有原始的PropertySource替换成自己的EncryptablePropertySource。这个代理对象会在每次获取属性值时,先检查是否是加密值(以ENC()包裹),如果是就解密后再返回。
java复制// 这是Jasypt替换PropertySource的关键代码
public void convertPropertySources(MutablePropertySources propSources) {
propSources.stream()
.filter(ps -> !(ps instanceof EncryptablePropertySource))
.map(this::makeEncryptable)
.collect(toList())
.forEach(ps -> propSources.replace(ps.getName(), ps));
}
Jasypt提供了两种代理方式:Wrapper模式和Proxy模式。Wrapper模式会为每种PropertySource创建对应的包装类,比如EncryptableMapPropertySourceWrapper。Proxy模式则使用Spring AOP动态生成代理对象。
Wrapper模式的优点是性能更好,因为不需要动态代理的开销。但它的缺点是需要为每种PropertySource类型实现对应的包装类。Proxy模式更灵活,可以处理任何类型的PropertySource,但性能稍差。
java复制private <T> PropertySource<T> convertPropertySource(PropertySource<T> propertySource) {
return interceptionMode == InterceptionMode.PROXY
? proxyPropertySource(propertySource)
: instantiatePropertySource(propertySource);
}
在实际项目中,我发现Wrapper模式对大多数场景已经足够,除非你使用了非常特殊的PropertySource实现。
首先,我们需要在pom.xml中添加Jasypt的依赖:
xml复制<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
然后在application.properties中配置加密密钥:
properties复制jasypt.encryptor.password=mySecretKey
现在,你可以使用Jasypt提供的工具类加密你的敏感配置了。假设你的数据库密码是"mydbpassword",加密后的结果可能是"ENC(tgjLqtpTOf7z6IC+xE9r+w==)",把这个值放到配置文件中:
properties复制spring.datasource.password=ENC(tgjLqtpTOf7z6IC+xE9r+w==)
Spring Boot在启动时会自动解密这个值,你的应用代码拿到的已经是解密后的"mydbpassword"了。
直接把加密密钥写在配置文件里,相当于把钥匙和锁放在一起,这显然不够安全。我推荐以下几种更安全的密钥管理方式:
通过环境变量传递密钥:
bash复制export JASYPT_ENCRYPTOR_PASSWORD=mySecretKey
java -jar your-application.jar
使用JVM参数:
bash复制java -Djasypt.encryptor.password=mySecretKey -jar your-application.jar
在云环境中使用密钥管理服务,比如AWS的KMS或Azure的Key Vault。
我曾经在一个金融项目中,我们甚至实现了动态密钥轮换:应用启动时从HSM(硬件安全模块)获取当前有效的密钥,定期自动轮换。这样即使某个密钥泄露,影响范围也被控制在最小范围内。
虽然Jasypt默认提供了PBE、AES等加密算法,但有时我们需要使用特定的加密方案。比如有些公司有自己开发的加密库,或者需要符合特定的安全标准。
要实现自定义加密,只需要实现StringEncryptor接口:
java复制public class MyCustomEncryptor implements StringEncryptor {
@Override
public String encrypt(String message) {
// 实现你的加密逻辑
return "ENC(" + customEncrypt(message) + ")";
}
@Override
public String decrypt(String encryptedMessage) {
// 实现你的解密逻辑
return customDecrypt(encryptedMessage.substring(4, encryptedMessage.length()-1));
}
}
然后在配置中指定使用你的加密器:
java复制@Bean("jasyptStringEncryptor")
public StringEncryptor stringEncryptor() {
return new MyCustomEncryptor();
}
有时候默认的代理逻辑不能满足需求。比如你可能想记录哪些配置项被解密了,或者想根据配置项的名称决定是否解密。
这时你可以继承EncryptablePropertySourceWrapper并重写getProperty方法:
java复制public class LoggingPropertySourceWrapper<T> extends EncryptablePropertySourceWrapper<T> {
public LoggingPropertySourceWrapper(PropertySource<T> source,
PropertyResolver resolver,
PropertyFilter filter) {
super(source, resolver, filter);
}
@Override
public Object getProperty(String name) {
Object value = super.getProperty(name);
if (value != null && value.toString().startsWith("ENC(")) {
logger.info("Decrypted configuration property: " + name);
}
return value;
}
}
然后修改PropertySource转换逻辑,使用你的包装类:
java复制private <T> PropertySource<T> instantiatePropertySource(PropertySource<T> propertySource) {
if (propertySource instanceof MapPropertySource) {
return (PropertySource<T>) new LoggingPropertySourceWrapper(
(MapPropertySource) propertySource, propertyResolver, propertyFilter);
}
// 其他类型的处理...
}
虽然单次解密的开销不大,但如果你的应用频繁读取加密配置,可能会成为性能瓶颈。Jasypt已经考虑到了这点,它使用了多级缓存:
如果你发现配置读取变慢,可以调整这些缓存参数:
properties复制jasypt.encryptor.pool-size=10 # 加密器池大小
jasypt.encryptor.cache-seconds=300 # 缓存时间
问题1:启动时报解密失败
这通常是因为加密密钥不正确。检查:
问题2:部分配置项没有解密
检查:
问题3:加密结果每次不同
这是PBE加密的正常现象,它使用了随机盐值。只要解密结果一致就不用担心。
我在一个微服务项目中遇到过一个问题:某个服务能正常解密配置,但另一个服务却失败。最后发现是因为两个服务使用的Jasypt版本不同,新版本默认使用了更强的加密算法。解决方案是显式指定算法:
properties复制jasypt.encryptor.algorithm=PBEWithMD5AndDES
配置加密只是安全防护的一环,要构建真正安全的系统,还需要考虑以下方面:
我曾经参与过一个政府项目,安全要求特别严格。我们不仅加密了所有配置,还实现了配置访问的白名单机制:每个服务只能读取它明确声明需要的配置项,其他配置即使存在也无法访问。这大大减少了配置泄露的风险面。