每个应用都有很多配置项,有些配置项对外非常敏感,例如数据库连接密码、私钥等。使用明文存在泄露的风险,生产环境要配合加密算法。jasypt是一个方便、流行的加密工具,支持PBE、AEC和对称加密。它与spring-boot的集成度很高,可以方便的为spring-boot属性进行加解密。
要使用jayspt要引入pom文件:
org.jasypt
jasypt
1.9.3
以PBE加密为例,要先初始化Encryptor
对象,然后对“abc”进行加解密。
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的配置项是放在PropertySource里,jasypt通过生成PropertySource的代理对象。后面从PropertySource获取属性时,先通过jasypt先解密,再返回解密后的值。
下面再看一下jasypt具体是怎么实现的代理,先引入对应的pom文件:
com.github.ulisesbocchio
jasypt-spring-boot-starter
3.0.5
这个starter通过SPI的方式,自动加载了EnableEncryptablePropertiesConfiguration
配置类,这个类里注册了jasypt自己的BeanFactoryPostProcessor
。它里面的功能就是拿到ConfigurableEnvironment
里的MutablePropertySources
,MutablePropertySources
里包含所有的PropertySource。把PropertySource包装成自己的EncryptablePropertySource
,然后替换掉原来的PropertySource。
// 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 PropertySource convertPropertySource(PropertySource propertySource) {
return interceptionMode == InterceptionMode.PROXY
? proxyPropertySource(propertySource) : instantiatePropertySource(propertySource);
}
// Proxy会通过Spring的AOP生成代理对象
private PropertySource proxyPropertySource(PropertySource 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) proxyFactory.getProxy();
}
// Wrapper会跟进原始的PropertySource类型,生成对应的包装类
private PropertySource instantiatePropertySource(PropertySource propertySource) {
PropertySource encryptablePropertySource;
if (needsProxyAnyway(propertySource)) {
encryptablePropertySource = proxyPropertySource(propertySource);
} else if (propertySource instanceof SystemEnvironmentPropertySource) {
encryptablePropertySource = (PropertySource) new EncryptableSystemEnvironmentPropertySourceWrapper((SystemEnvironmentPropertySource) propertySource, propertyResolver, propertyFilter);
} else if (propertySource instanceof MapPropertySource) {
encryptablePropertySource = (PropertySource) 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;
}
后面从MutablePropertySources
获取属性的时候,实际上调用的是EncryptablePropertySource
的Object getProperty(String name)
方法。
以applicaiton.properties文件里的配置项为例,它是OriginTrackedMapPropertySource
类型,属于MapPropertySource
的子类,会被jasypt包装成EncryptableMapPropertySourceWrapper
。
想在获取OriginTrackedMapPropertySource
里的属性,会先调用EncryptableMapPropertySourceWrapper
的getProperty(String name)
方法。
EncryptableMapPropertySourceWrapper
会将请求转交给代理对象CachingDelegateEncryptablePropertySource
。因为CachingDelegateEncryptablePropertySource
会将解析的结果放到Map
缓存对象里,避免重复解析。
然后,CachingDelegateEncryptablePropertySource
会将加密内容交给DefaultLazyPropertyResolver
解密配置项。从名字就可以看出,DefaultLazyPropertyResolver
是为了延迟初始化,实际上它包装了DefaultPropertyResolver
,这是默认jasypt的解析器。
DefaultPropertyResolver
先加加密内容执行spring的占位符解析,例如解析${my.password},得到“ENC(tgjLqtpTOf7z6IC+xE9r+w==)”,再对”ENC(tgjLqtpTOf7z6IC+xE9r+w==)”进行解密。
DefaultPropertyResolver
内部调用DefaultLazyEncryptor
,通过延迟加载的方式得到默认的加密机类型。这时会根据上下文配置,选择是使用PBE、AEC还是对称加密。
// 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为例,这里方法的就是PooledPBEStringEncryptor
对象。它内部有多个StandardPBEStringEncryptor
组成一个池子,可以提升加密性能。最后会按顺序获得一个StandardPBEStringEncryptor
,调用decrypt(final String encryptedMessage)
方法进行解密。
jasypt通过BeanFactoryPostProcessor
代理了所有的PropertySource,在获取属性之前,对属性进行解密。过程中涉及到了几个对象:
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