背景
每个应用都有很多配置项,有些配置项对外非常敏感,例如数据库连接密码、私钥等。使用明文存在泄露的风险,生产环境要配合加密算法。jasypt是一个方便、流行的加密工具,支持PBE、AEC和对称加密。它与spring-boot的集成度很高,可以方便的为spring-boot属性进行加解密。
使用方式
要使用jayspt要引入pom文件:
<dependency> <groupId>org.jasypt</groupId> <artifactId>jasypt</artifactId> <version>1.9.3</version> </dependency>
以PBE加密为例,要先初始化
public class EncryptionDemo { public static void main(String[] args) { StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor(); SimpleStringPBEConfig config = new SimpleStringPBEConfig(); config.setPassword("123"); encryptor.setConfig(config); String rawMsg = "abc"; String encryptedMsg = encryptor.encrypt(rawMsg); System.out.println(rawMsg + " = " + encryptedMsg); // abc = tgjLqtpTOf7z6IC+xE9r+w== String decryptedMsg = encryptor.decrypt(encryptedMsg); System.out.println(encryptedMsg + " = " + decryptedMsg); // tgjLqtpTOf7z6IC+xE9r+w== = abc } }
可以看到“abc”加密后结果是“tgjLqtpTOf7z6IC+xE9r+w==”,解密后还是“abc”。并且同样的“abc”参数,每次加密的结果都不一样,但解密的结果是一样的。
集成spring-boot
加解密功能已经有了,接下来怎么集成到spring-boot上,将spring-boot里的配置项自动解密?
由于spring-boot的配置项是放在PropertySource里,jasypt通过生成PropertySource的代理对象。后面从PropertySource获取属性时,先通过jasypt先解密,再返回解密后的值。
下面再看一下jasypt具体是怎么实现的代理,先引入对应的pom文件:
<dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>3.0.5</version> </dependency>
生成代理对象
这个starter通过SPI的方式,自动加载了
// com.ulisesbocchio.jasyptspringboot.EncryptablePropertySourceConverter public void convertPropertySources(MutablePropertySources propSources) { propSources.stream() .filter(ps -> !(ps instanceof EncryptablePropertySource)) // makeEncryptable方法会将PropertySource替换成EncryptablePropertySource的子类 .map(this::makeEncryptable) .collect(toList()) .forEach(ps -> propSources.replace(ps.getName(), ps)); } // 这里还分两种代理方式,一种是Proxy,一个是Wrapper private <T> PropertySource<T> convertPropertySource(PropertySource<T> propertySource) { return interceptionMode == InterceptionMode.PROXY ? proxyPropertySource(propertySource) : instantiatePropertySource(propertySource); } // Proxy会通过Spring的AOP生成代理对象 private <T> PropertySource<T> proxyPropertySource(PropertySource<T> propertySource) { //can't be proxied with CGLib because of methods being final. So fallback to wrapper for Command Line Arguments only. if (CommandLinePropertySource.class.isAssignableFrom(propertySource.getClass()) // Other PropertySource classes like org.springframework.boot.env.OriginTrackedMapPropertySource // are final classes as well || Modifier.isFinal(propertySource.getClass().getModifiers())) { return instantiatePropertySource(propertySource); } ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.setTargetClass(propertySource.getClass()); proxyFactory.setProxyTargetClass(true); proxyFactory.addInterface(EncryptablePropertySource.class); proxyFactory.setTarget(propertySource); proxyFactory.addAdvice(new EncryptablePropertySourceMethodInterceptor<>(propertySource, propertyResolver, propertyFilter)); return (PropertySource<T>) proxyFactory.getProxy(); } // Wrapper会跟进原始的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; }
后面从
解析属性流程
以applicaiton.properties文件里的配置项为例,它是
想在获取
然后,
// com.ulisesbocchio.jasyptspringboot.configuration.StringEncryptorBuilder public StringEncryptor build() { // 包含jasypt.encryptor.password就使用PBE if (isPBEConfig()) { return createPBEDefault(); // 包含jasypt.encryptor.privateKeyString相关的配置就使用非对称加密 } else if (isAsymmetricConfig()) { return createAsymmetricDefault(); // 包含jasypt.encryptor.gcmSecretKeyString相关配置就使用GCM加密 } else if (isGCMConfig()) { return createGCMDefault(); } else { throw new IllegalStateException("either '" + propertyPrefix + ".password', one of ['" + propertyPrefix + ".private-key-string', '" + propertyPrefix + ".private-key-location'] for asymmetric encryption, or one of ['" + propertyPrefix + ".gcm-secret-key-string', '" + propertyPrefix + ".gcm-secret-key-location', '" + propertyPrefix + ".gcm-secret-key-password'] for AES/GCM encryption must be provided for Password-based or Asymmetric encryption"); } }
以PBE为例,这里方法的就是
总结
jasypt通过
EncryptablePropertySource 是PropertySource的包装类CachingDelegateEncryptablePropertySource 缓存了解密结果EncryptableMapPropertySourceWrapper 是MapPropertySource 的包装类DefaultPropertyResolver 先解析Spring的占位符DefaultLazyPropertyResolver 实现了延迟加载DefaultPropertyResolver 对象StandardPBEStringEncryptor 是PBE加密机PooledPBEStringEncryptor 通过池化StandardPBEStringEncryptor 提升并发度
引用
jasypt的github:https://github.com/ulisesbocchio/jasypt-spring-boot?tab=readme-ov-file