随笔 - 145  文章 - 0  评论 - 6  阅读 - 18万

JDBC连接池密码加密及Spring Boot扩展机制

前言

如果想要在application.yaml文件中配置的密码是一个密文,并且数据库连接池在初始化时可以正常的拿到连接,那么我们便要在连接池初始化前将密文变成明文。下面将使用Spring提供的几个扩展机制来实现这件事

方案1: BeanFactoryPostProcessor

BeanFactoryPostProcessor可以让我们拿到DataSourceBeanDefinition,这样我们便可以修改属性了。

@Component
public class DataSourceBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        String[] beanNames = beanFactory.getBeanNamesForType(DataSource.class);
        for (String beanName : beanNames) {
            BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
            if (beanDefinition.getPropertyValues().contains("password")) {
                // 获取密文
                Object encryptPassword = beanDefinition.getPropertyValues().get("password");
                if (encryptPassword != null) {
                    // 解密获取明文(这里方便起见使用的Base64编码)
                    String realPassword = Base64.decodeStr(encryptPassword.toString());
                    // 覆盖属性
                    beanDefinition.getPropertyValues().add("password", realPassword);
                }
            }
        }
    }
}

该代码只适用于以前古老的使用xml配置DataSource的场景,即下面配置

<bean id="dataSource" class="org.apache.tomcat.jdbc.pool.DataSource">
    <!-- 数据库驱动 -->
    <property name="driverClassName" value="${db.driverClass}"/>
    <!-- 数据库地址 -->
    <property name="url" value="${db.url}"/>
    <!-- 数据库用户名 -->
    <property name="username" value="${db.username}"/>
    <!-- 数据库密码 -->
    <property name="password" value="xxxxx"/>
  </bean>

因为这么配置下面这段代码才能拿到密文

// 获取密文
Object encryptPassword = beanDefinition.getPropertyValues().get("password");

而目前一般都使用Spring Boot,自动装配使用的Java配置,是拿不到值的。因此稍微改写从Environment实例中去拿密文。

@Component
public class DataSourceBeanFactoryPostProcessor implements BeanFactoryPostProcessor, EnvironmentAware {

    private Environment environment;

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        String[] beanNames = beanFactory.getBeanNamesForType(DataSource.class);
        for (String beanName : beanNames) {
            BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
            String encryptPassword = environment.getProperty("spring.datasource.password");
            if (encryptPassword != null) {
                // 解密获取明文(这里方便起见使用的Base64编码)
                String realPassword = Base64.decodeStr(encryptPassword);
                // 添加属性(即会调用set方法赋值)
                beanDefinition.getPropertyValues().add("password", realPassword);
            }
        }
    }

    @Override
    public void setEnvironment(@NonNull Environment environment) {
        this.environment = environment;
    }
}

使用该种方式有一个问题,如果Bean有使用@ConfigurationProperties来装配属性,那么将会被再次覆盖,因为Environment中的属性并没有被修改。而@ConfigurationProperties绑定属性是在bean通过set方法赋值完了之后再装配的,绑定时机是BeanPostProcessor.postProcessBeforeInitialization

幸好连接池虽然使用了@ConfigurationProperties来装配其他属性,但是不包括密码,所以不会被覆盖。

static class Tomcat {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.tomcat")
    org.apache.tomcat.jdbc.pool.DataSource dataSource(DataSourceProperties properties) {
        org.apache.tomcat.jdbc.pool.DataSource dataSource = createDataSource(properties,
                org.apache.tomcat.jdbc.pool.DataSource.class);
        DatabaseDriver databaseDriver = DatabaseDriver.fromJdbcUrl(properties.determineUrl());
        String validationQuery = databaseDriver.getValidationQuery();
        if (validationQuery != null) {
            dataSource.setTestOnBorrow(true);
            dataSource.setValidationQuery(validationQuery);
        }
        return dataSource;
    }

}

可以看到装配的属性前缀是spring.datasource.tomcat,而我们配置属性时是这样。

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1/test
    username: root
    password: MTIzNDU2
    tomcat:
      initial-size: 5
      max-active: 100
      min-idle: 5

所以环境中并没有spring.datasource.tomcat.password属性,因此就不会被覆盖了。

方案二: 直接修改Environment

可参考Spring Boot环境扩展机制

Spring Boot在启动时提供了扩展机制,用于定制Environment,并且执行时机是在上下文创建之前,因此该方案基本能应对所有情况。

public class DataSourceEnvPostProcessor implements EnvironmentPostProcessor, Ordered {

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        String encryptPassword = environment.getProperty("spring.datasource.password");
        // 解密获取明文(这里方便起见使用的Base64编码)
        String realPassword = Base64.decodeStr(encryptPassword);
        Map<String, Object> map = new HashMap<>();
        map.put("spring.datasource.password", realPassword);
        PropertySource<?> dbPasswordPropertySource = new MapPropertySource("dbPassword", map);
        // 优先级最高, 起到属性覆盖作用
        environment.getPropertySources().addFirst(dbPasswordPropertySource);
    }

    /**
     * 最低优先级执行, 低于别的EnvironmentPostProcessor实例执行, 以便可以拿到spring.datasource.password属性
     * 加载application.yml文件的EnvironmentPostProcessor是ConfigFileApplicationListener
     */
    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}

MEAT-INF/spring.factories

org.springframework.boot.env.EnvironmentPostProcessor=\  
com.wangtao.springboottest.env.DataSourceEnvPostProcessor

指定自定义的EnvironmentPostProcessor,使得Spring Boot去加载它。

该方案注意点就是自定义的EnvironmentPostProcessor执行顺序要低于加载application.yml文件的EnvironmentPostProcessor,以便可以拿到application.yml文件配置的属性,然后将自己定义的PropertySource放到列表最前面,起到属性覆盖作用。

方案三: ApplicationContextInitializer扩展

ApplicationContextInitializer执行时机是ApplicationContext创建后执行(此时refresh方法还未调用),此时Environment实例已经全部初始化完了(所有的EnvironmentPostProcessor全部执行完毕)。

本来方案二已经很完美了,但是微服务项目一般使用配置中心,如果把密码放到配置中心中,方案二将失效,因为配置中心的加载时机是在ApplicationContextInitializer执行的,晚于EnvironmentPostProcessor,因此我们将拿不到spring.datasource.password的属性值,导致方案二失效。

于是使用ApplicationContextInitializer方案便出来了。

public class DataSourceApplicationContextInitializer implements Ordered,
        ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Override
    public void initialize(@NonNull ConfigurableApplicationContext applicationContext) {
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        String encryptPassword = environment.getProperty("spring.datasource.password");
        // 解密获取明文(这里方便起见使用的Base64编码)
        String realPassword = Base64.decodeStr(encryptPassword);
        Map<String, Object> map = new HashMap<>();
        map.put("spring.datasource.password", realPassword);
        PropertySource<?> dbPasswordPropertySource = new MapPropertySource("dbPassword", map);
        // 优先级最高, 起到属性覆盖作用
        environment.getPropertySources().addFirst(dbPasswordPropertySource);
    }

    /**
     * 最低优先级, 低于加载配置中心的ApplicationContextInitializer即可
     * PropertySourceBootstrapConfiguration
     */
    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}

MEAT-INF/spring.factories

org.springframework.context.ApplicationContextInitializer=\  
com.wangtao.springboottest.config.DataSourceApplicationContextInitializer

指定自定义的ApplicationContextInitializer,使得Spring Boot去加载它。

该方案本质上与EnvironmentPostProcessor没有什么区别,只是执行时机稍微晚了一点点。

posted on   wastonl  阅读(1715)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

点击右上角即可分享
微信分享提示