最近研究了jasypt-spring-boot的源码,它的原理很简单,首先对配置文件的敏感信息进行加密,然后拦截配置文件的加载过程,获取配置文件的信息,并对加过密的配置使用与加密相同的算法进行解密就可以了。加密和解密的算法so easy,不提也罢。关键是配置文件的拦截过程,这个才是重点。
第一步:
先把准备工作做好,首先在@Configuration类中使用配置三个Bean。
@Bean
public StringEncryptor stringEncryptor() {
return new DefaultLazyEncryptor();
}
@Bean
public EncryptablePropertyDetector encryptablePropertyDetector() {
return new DefaultLazyPropertyDetector();
}
@Bean
public EncryptablePropertyResolver encryptablePropertyResolver(EncryptablePropertyDetector propertyDetector, StringEncryptor encryptor) {
return new DefaultLazyPropertyResolver(propertyDetector, encryptor);
}
说明:StringEncryptor会对加过密的配置进行解密。
EncryptablePropertyDetector会对加过密的配置进行去除前缀和后缀的操作。
EncryptablePropertyResolver会同时使用StringEncryptor和EncrypatblePropertyDetector,会先对加过密的配置进行去除前缀和后缀的操作,然后对其进行解密操作。
例如:配置JIA(DuafkhAH47w=),EncryptablePropertyResolver会使用EncryptablePropertyDetector的某个方法判断该配置是否以”JIA(“开头并且以”)”结尾。如果是,就对该配置进行去除前缀和后缀的操作,得到”DuafkhAH47w=”,并对这个值进行解密操作。
第二步需要配置EnableEncryptablePropertiesBeanFactoryPostProcessor这个bean。
EnableEncryptablePropertiesBeanFactoryPostProcessor这个类会使用第一步配置好的EncryptablePropertyResolver对拦截到的配置进行解密,并把解密的结果替换掉解密之前的值。这一步的核心是如何对配置进行拦截。
这个类的源码如下:
public class EnableEncryptablePropertiesBeanFactoryPostProcessor implements BeanFactoryPostProcessor, ApplicationListener<ApplicationEvent>, Ordered{
private static final Logger log = LoggerFactory.getLogger(EnableEncryptablePropertiesBeanFactoryPostProcessor.class);
private ConfigurableEnvironment environment;
private InterceptionMode interceptionMode;
public EnableEncryptablePropertiesBeanFactoryPostProcessor(ConfigurableEnvironment environment,
InterceptionMode interceptionMode) {
this.environment = environment;
this.interceptionMode = interceptionMode;
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
/**
* ApplicationEvent event是所要监听的对象
*/
@Override
public void onApplicationEvent(ApplicationEvent event) {
log.debug("Application Event Raised: {}", event.getClass().getSimpleName());
}
/**
* 被类被spring容器加载的时候,该方法会执行
*/
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
log.info("Post-processing PropertySource instances");
/**
* 使用EncryptablePropertyResolver对象来对加过密的配置进行解密。
*/
EncryptablePropertyResolver propertyResolver = beanFactory.getBean(EncryptablePropertyResolver.class);
MutablePropertySources propSources = environment.getPropertySources();
EncryptablePropertySourceConverter.convertPropertySources(interceptionMode, propertyResolver, propSources);
}
}
由于的这个过程过于复杂,所以实现了BeanFactoryPostProcessor接口,把具体的实现委托给了EncryptablePropertySourceConverter这个类来处理了。
这个类的源码如下:
public class EncryptablePropertySourceConverter {
private static final Logger log = LoggerFactory.getLogger(EncryptablePropertySourceConverter.class);
public static void convertPropertySources(InterceptionMode interceptionMode, EncryptablePropertyResolver propertyResolver, MutablePropertySources propSources) {
StreamSupport.stream(propSources.spliterator(), false)
/*过滤掉EncryptablePropertySource类型的MutablePropertySources*/
.filter(ps -> !(ps instanceof EncryptablePropertySource))
.map(ps -> makeEncryptable(interceptionMode, propertyResolver, ps))
.collect(Collectors.toList())
.forEach(ps -> propSources.replace(ps.getName(), ps));
}
@SuppressWarnings("unchecked")
public static <T> PropertySource<T> makeEncryptable(InterceptionMode interceptionMode, EncryptablePropertyResolver propertyResolver, PropertySource<T> propertySource) {
//又检查了一次
if (propertySource instanceof EncryptablePropertySource) {
return propertySource;
}
PropertySource<T> encryptablePropertySource = convertPropertySource(interceptionMode, propertyResolver, propertySource);
log.info("Converting PropertySource {} [{}] to {}", propertySource.getName(), propertySource.getClass().getName(),
AopUtils.isAopProxy(encryptablePropertySource) ? "AOP Proxy" : encryptablePropertySource.getClass().getSimpleName());
return encryptablePropertySource;
}
/**
* 使用拦截器对属性值进行拦截,并对属性值进行解密操作。
* @param interceptionMode
* @param propertyResolver
* @param propertySource
* @return
*/
private static <T> PropertySource<T> convertPropertySource(InterceptionMode interceptionMode, EncryptablePropertyResolver propertyResolver, PropertySource<T> propertySource) {
return interceptionMode == InterceptionMode.PROXY
? proxyPropertySource(propertySource, propertyResolver) : instantiatePropertySource(propertySource, propertyResolver);
}
/**
* 使用了cglib代理对属性源进行了拦截和机密
* @param propertySource
* @param resolver
* @return
*/
@SuppressWarnings("unchecked")
public static <T> PropertySource<T> proxyPropertySource(PropertySource<T> propertySource, EncryptablePropertyResolver resolver) {
//Silly Chris Beams for making CommandLinePropertySource getProperty and containsProperty methods final. Those methods
//can't be proxied with CGLib because of it. So fallback to wrapper for Command Line Arguments only.
if (CommandLinePropertySource.class.isAssignableFrom(propertySource.getClass())) {
return instantiatePropertySource(propertySource, resolver);
}
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.setTargetClass(propertySource.getClass());
proxyFactory.setProxyTargetClass(true);
proxyFactory.addInterface(EncryptablePropertySource.class);
proxyFactory.setTarget(propertySource);
proxyFactory.addAdvice(new EncryptablePropertySourceMethodInterceptor<>(propertySource, resolver));
return (PropertySource<T>) proxyFactory.getProxy();
}
/**
* PropertySource的名字匹配
* "org.springframework.boot.context.config.ConfigFileApplicationListener$ConfigurationPropertySources",
* "org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertySource"
* 当中的一个,就使用cglib动态代理的方式,对属性值进行拦截和解密。
*
* 如果不匹配,就使用各种类型的PropertySourceWrapper对不同类型的属性源进行包装解密。(使用包装器来对属性进行解密)
* @param propertySource
* @param resolver
* @return
*/
@SuppressWarnings("unchecked")
public static <T> PropertySource<T> instantiatePropertySource(PropertySource<T> propertySource, EncryptablePropertyResolver resolver) {
PropertySource<T> encryptablePropertySource;
if (needsProxyAnyway(propertySource)) {
encryptablePropertySource = proxyPropertySource(propertySource, resolver);
} else if (propertySource instanceof MapPropertySource) {
encryptablePropertySource = (PropertySource<T>) new EncryptableMapPropertySourceWrapper((MapPropertySource) propertySource, resolver);
} else if (propertySource instanceof EnumerablePropertySource) {
encryptablePropertySource = new EncryptableEnumerablePropertySourceWrapper<>((EnumerablePropertySource) propertySource, resolver);
} else {
encryptablePropertySource = new EncryptablePropertySourceWrapper<>(propertySource, resolver);
}
return encryptablePropertySource;
}
@SuppressWarnings("unchecked")
private static boolean needsProxyAnyway(PropertySource<?> ps) {
return needsProxyAnyway((Class<? extends PropertySource<?>>) ps.getClass());
}
private static boolean needsProxyAnyway(Class<? extends PropertySource<?>> psClass) {
return needsProxyAnyway(psClass.getName());
}
/**
* Some Spring Boot code actually casts property sources to this specific type so must be proxied.
*/
private static boolean needsProxyAnyway(String className) {
return Stream.of(
"org.springframework.boot.context.config.ConfigFileApplicationListener$ConfigurationPropertySources",
"org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertySource"
).anyMatch(className::equals);
}
}
上面的处理逻辑是这样的:如果使用的是代理的模式,就使用spring封装的cglib代理类来对属性值进行解密处理。如果使用的是包装器的模式,就分别对不同类型的属性源使用不同的包装器对它们进行解密操作。
在微服务的开发模式下,一般不使用上面介绍的方式对配置进行加密处理,一般会使用配置中心,来集中对所有的配置进行处理,然后各个服务的项目通过http或其他方式来请求配置中心的配置。
下一周我会把的这个问题解决并汇总,写出博客。
又想相互交流相互学习的同学,可以加这个qq群:313145288