现在业务开发基本都要和数据库打交道,那么第一步就是连接数据库,当然数据库连接过程有很多开源实现,我们也就不用在和底层数据库驱动打交道,更不用去手动管理连接和释放连接等等,只需一些简单配置即可。在配置数据库时,一般我们都是明文配置在工程文件中。像数据库连接池一些基础属性明文配置也就无所谓了,但是对于数据库用户名和密码如果也这样配置就会存在安全隐患。因为团队内接触过该工程的人都可以看到用户名和密码。如果团队小还基本可控,也就那么几个人可以接触到,如果团队人数比较多,如果还采用这种明文方式,那基本失控了,数据将毫无安全性可言。本文介绍一种相对安全的方案,其实就是把数据库数据源管理做成一个starter工程,该服务在启动时会调用数据库配置信息下发接口,在线获取用户名和密码。这样做的好处主要有两点,第一,数据库用户名和密码等信息通过管理后台由专人管理和维护,维护人员可以是dba或者运维,尽量缩小接触这些敏感信息的人员范围;第二,该数据源服务可以供公司不同业务组使用,也就是配置时会根据不同的数据库名称,创建对应的数据源并注入到容器,供业务使用。最终达到底层服务多方复用,敏感信息集中管理的目的。

       那么结合实际工作,具体实现就是编写一个数据库数据源管理的spring boot starter工程。starter可以理解为一个可拔插式的插件,例如,你想使用jdbc插件,那么可以使用spring-boot-starter-jdbc;如果想使用redis,可以使用spring-boot-starter-data-redis。spring boot项目就是由一个个starter组成。一般情况下,如果想把某些功能内聚到一起,形成一个独立工程,然后对外提供某个服务时,starter就派上用场了。比如本文介绍的数据源管理。

       自定义一个starter:

       这块网上资料很多,文本不做赘述,基本就是新建一个spring boot工程,然后新建配置文件类、核心服务类、自动配置类及定义spring.factories属性文件。最后打包输出,供其他spring boot依赖、调用。简单归简单,但还是建议手动编写一个属于你自己的spring boot starter “hello world”,毕竟纸上得来终觉浅,自己动手写一个那才真正是你的。

       starter工作原理:

       1.spring boot在启动时扫描项目所依赖的JAR包,寻找包含spring.factories文件的JAR包;

       2.读取spring.factories文件获取配置的自动配置类AutoConfiguration;

       3.将自动配置类下满足条件(@ConditionalOnXxx)的@Bean放入到Spring容器中(Spring Context);

       4.使用者可以直接用来注入并使用该服务,因为该类已经在容器中了。

       不过本文介绍的数据源管理方案并没有写成一个标准的starter工程,毕竟只是根据数据库名称,查询数据库用户名和密码等信息,然后创建数据源。下面是一些源码示例:

       数据源定义:

public class SecureDataSourceFactoryBean extends AbstractFactoryBean<DataSource> {

    private String database;
    private String decryptKey;
    private String propsUrl;

    private String username;
    private String password;

    public SecureDataSourceFactoryBean(String decryptKey, String propsUrl) {
        this.decryptKey = decryptKey;
        this.propsUrl = propsUrl;
    }

    @Override
    public Class<?> getObjectType() {
        return DataSource.class;
    }

    @Override
    protected DataSource createInstance() throws Exception {
        final DruidDataSource source = initDruidDataSource();

        source.setInitialSize(initialSize);
        source.setMinIdle(minIdle);
        source.setMaxActive(maxActive);
        source.setMaxWait(maxWait);
        source.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
        source.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        source.setValidationQuery(validationQuery);
        source.setTestWhileIdle(testWhileIdle);
        source.setTestOnBorrow(testOnBorrow);
        source.setTestOnReturn(testOnReturn);
        source.setPoolPreparedStatements(poolPreparedStatements);
        source.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);

        source.setRemoveAbandoned(removeAbandoned);
        source.setRemoveAbandonedTimeoutMillis(removeAbandonedTimeoutMillis);
        source.setLogAbandoned(logAbandoned);
        source.setConnectionInitSqls(connectionInitSqls);

        source.addFilters(filters);

        source.init();
        return source;
    }

    private DruidDataSource initDruidDataSource() throws Exception {
        final DruidDataSource source = new DruidDataSource();
        if (isEmpty(database) || isEmpty(propsUrl) || isEmpty(decryptKey)) {
            throw new IllegalArgumentException("[Assertion failed] - (database, propsUrl2, decryptKey) parameters is required; it must not be null");
        }

        String remoteUrl = propsUrl + "/" + database;
        JdbcProperties props;
        try {
            props = REST.getWithRetry(remoteUrl, HTTP_TIMEOUT, JdbcProperties.class);
        } catch (Exception e){
            String err = String.format("cause : may be is missing database %s or not safe period get user/pwd", database);
            log.error(err);
            throw new Exception(err, e);
        }
        source.setUrl(this.buildUrl(props));
        source.setUsername(props.getUsername());
        source.setPassword(props.getPassword());

        source.addFilters("config");
        source.setConnectionProperties("config.decrypt=true;config.decrypt.key=" + decryptKey);
        source.setName(escapeName(database));

        return source;
    }
}

       业务方在接入时,按照下面方式创建数据源即可,同时在配置文件中添加一些数据源设置,当然也需要在管理后台配置数据库用户名、密码:

@Configuration
public class DataSourceConfig extends SecureDataSourceAutoConfiguration {

    @Bean
    @ConfigurationProperties("spring.secure.ds.db_name")
    FactoryBean<DataSource> testDataSource() {
        return createSecureDataSourceFactoryBean();
    }
}

       数据源设置

spring.secure.ds.db_name.database=test001
spring.secure.ds.db_name.connectionInitSqls=set names utf8mb4
spring.secure.ds.db_name.propsUrl=https://test.database.info/props
spring.secure.ds.db_name.decryptKey=SAJBAIt7QErvOg8b