一、背景
很多项目的配置都写到了应用的配置文件中,或者通过配置中心的项目空间自己管理,这样是不太安全的,并且有相当一部分中小型团队,对于访问线上数据库、redis等资源没有做网段限制,开发人员或者能够看到项目配置的产研人员可以直接访问线上资源,这是比较危险的,有时候会直接本地连接线上资源进行操作,也难免会出现误操作导致线上数据丢失或者损坏,进而导致不能恢复或者很难恢复问题,从而给产品结构带来不可挽回的损失。
对于线上环境的一些核心配置,就以数据资源为例,可以收敛到运维或者中间件团队集中管理,对一线开发人员不直接暴露连接地址和账密,并且中间件团队需要提供一种能力供业务服务使用,在业务服务启动时可以在线上机器读取数据库连接配置并初始化数据库连接。
前边文章中分析过《@ConfigurationProperties工作原理》,可以了解到核心能力由ConfigurationPropertiesBindingPostProcessor提供,我们可以参考其解析配置并绑定属性到bean的能力,在实例化线上资源连接的时候,去公共配置读取当前项目的相关配置,然后在实例化bean的时候,将相关敏感配置绑定进去,从而完成完整的线上资源连接实例化和初始化。
整体流程大致如下:
同样,考虑到通用性,将属性绑定的能力抽象到基础组件中,创建starter供业务应用依赖。
定义属性绑定注解,供业务应用定义资源时使用。
/**
*
* 参考 @ConfigurationProperties
*
* @author typhoon
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BootConfigurationProperties {
String value() default "";
}
编写抽象父类,定义通用方法和功能,供不同资源使用和实现。
/**
* 将公共配置初始化到环境变量中
*
* 实现参考了 {@link ConfigurationPropertiesBindingPostProcessor}
*
* @author Typhoon
*/
public abstract class BootConfigurationBeanPostProcessor implements BeanPostProcessor, PriorityOrdered,
ApplicationContextAware, InitializingBean, EnvironmentAware {
protected static final String PREFIX_DATASOURCE = "boot.datasource.";
protected Environment environment;
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public void afterPropertiesSet() throws Exception {
//从配置中心拉取公共配置,并添加到环境变量中,此处忽略获取配置实现
MapPropertySource source = new MapPropertySource(PROPERTY_SOURCE_NAME, configs);
environment.getPropertySources().addLast(source);
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
BootConfigurationProperties annotation = getAnnotation(bean, beanName,
BootConfigurationProperties.class);
if (annotation != null && match(annotation)) {
processBean(bean, beanName, annotation);
}
return bean;
}
/**
*是否匹配,子类实现
*/
public abstract boolean match(BootConfigurationProperties annotation);
/**
* 初始化前置处理,根据不同的配置交给子类去实现
*/
public abstract void processBean(Object bean, String beanName, BootConfigurationProperties annotation);
protected <A extends Annotation> A getAnnotation(Object bean, String beanName,
Class<A> type) {
A annotation = this.applicationContext.findAnnotationOnBean(beanName,type);
if (annotation == null) {
annotation = AnnotationUtils.findAnnotation(bean.getClass(), type);
}
return annotation;
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 1;
}
}
编写数据资源处理器子类实现:
public class DataSourceBeanPostProcessor extends BootConfigurationBeanPostProcessor {
@Override
public boolean match(BootConfigurationProperties annotation) {
return annotation.value().startsWith(PREFIX_DATASOURCE);
}
@Override
public void processBean(Object bean, String beanName, BootConfigurationProperties annotation) {
if(bean instanceof DataSource) {
new BootDataSourceFactory(environment).bind((DataSource)bean, annotation.value());
}
}
}
子类解析自定义注解的value是否是数据资源属性前缀,如果不是则不处理,如果是则bean初始化前置处理逻辑,此处继续检查定义的bean是否是DataSource,如果是才进行属性绑定。
绑定线上资源属性配置由BootDataSourceFactory的bind方法实现:
public void bind(DataSource bean, String propertyPrefix) {
//bindDatasource
DataSourceProperties properties = buildConfig(DataSourceProperties.class, propertyPrefix);
if (properties.getUrl() == null){
throw new IllegalStateException("datasource url is empty");
}
Map map = toMap(properties);
bind(bean, map);
//bindPool
EnvironmentConverter.bind(bean, environment, PROP_DATASOURCE_POOL_PREFIX);
// 支持特定的配置
String prefix = propertyPrefix + ".pool";
EnvironmentConverter.bind(bean, environment, prefix);
log.info("Datasource [{}] url init: {}", propertyPrefix, properties.getUrl());
log.info("Datasource [{}] user init: {}", propertyPrefix, properties.getUsername());
}
先从环境变量中获取资源相关配置并转换成DataSourceProperties,然后转换成Map类型:
private Map toMap(DataSourceProperties properties) {
Map map = new HashMap();
// 添加url和jdbc-url, 兼容Hikari和Druid
map.put("url", properties.getUrl());
map.put("jdbc-url", properties.getUrl());
map.put("driverClassName", properties.getDriverClassName());
map.put("username", properties.getUsername());
map.put("password", properties.getPassword());
return map;
}
这些就是数据资源连接常用的一些属性配置,从DataSourceProperties取出转换成Map,然后调用私有bind方法把Map中的属性绑定到DataSource类型的bean中:
private void bind(DataSource result, Map properties) {
ConfigurationPropertySource source = new MapConfigurationPropertySource(properties);
ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases();
aliases.addAliases("url", "jdbc-url", "jdbcurl");
aliases.addAliases("username", "user");
Binder binder = new Binder(source.withAliases(aliases));
binder.bind(ConfigurationPropertyName.EMPTY, Bindable.ofInstance(result));
}
最后,额外绑定一些连接池属性配置以及特定的配置。
经过这些处理,数据资源在初始化之前就通过属性绑定,注入了url、端口和账密等相关信息,等应用上下文刷新完成也就完成了数据资源连接的所有实例化和初始化动作,变成可用状态。
实现后的大致流程如下:
至于如何让我们自己定义的BeanPostProcessor生效,有很多方式,之前介绍过很多次,此处不做赘述。
在中间件的配置中心工作空间配置数据资源相关属性,并设置成共享类型,对其他应用不可见但是程序可读.
boot:
datasource:
example:
url: xxx
driverClassName: xxx
username: xxx
password: xxx
其中example是区分不同数据资源实例的名称。
在业务项目中引入通用组件依赖。
<dependency>
<groupId>com.example.boot</groupId>
<artifactId>example-boot-autoconfigure</artifactId>
</dependency>
@Configuration
public class DatasourceConfig {
@Bean("exampleDataSource")
@BootConfigurationProperties("boot.datasource.example")
public DataSource exampleDataSource() {
return BootDataSourceFactory.create();
}
}
使用BootConfigurationProperties注解定义数据资源,底层组件会根据boot.datasource.example寻找相关配置并注入到当前定义的DataSource中。
这样我们的业务应用中就可以使用数据资源了。
当然上述这一套方案,简化了程序开发复杂度,也在一定程度上保护了线上资源的敏感配置,但是此方案是防君子不防小人,虽然我们没办法从项目代码中和项目配置空间看到线上资源配置,但是这些属性配置必定是要被业务服务读取解析的,那么既然程序可以读取,那么程序也必定能够打印出来,懂得都懂。
日志打印是无法防范的,为了弥补这个缺陷,可以对线上资源设置一些入站规则和ip段限制,只允许线上某些机器可以访问,但是还会出现有些人有线上机器登录权限,使用线上资源做代理转发照样可以访问,对于这些问题需要运维和DBA解决了,比如线上机器禁用转发能力,线上资源关闭代理访问等等。
另外还可以结合环境变量识别程序运营的环境,如果是开发或者测试环境,直接走由自己控制的相关配置,如果是线上环境则统一去中间件获取。
本文分享自 PersistentCoder 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!