SpringBoot配置数据库密码加密的实现

你在使用 MyBatis 的过程中,是否有想过多个数据源应该如何配置,如何去实现?出于这个好奇心,我在 Druid Wiki 的数据库多数据源中知晓 Spring 提供了对多数据源的支持,基于 Spring 提供的 AbstractRoutingDataSource,可以自己实现数据源的切换。

一、配置动态数据源

下面就如何配置动态数据源提供一个简单的实现:

org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource,代码如下:

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

 @Nullable
 private Object defaultTargetDataSource;

 @Nullable
 private Map<Object, DataSource> resolvedDataSources;

 @Nullable
 private DataSource resolvedDefaultDataSource;

 @Override
 public Connection getConnection() throws SQLException {
 return determineTargetDataSource().getConnection();
 }

 @Override
 public Connection getConnection(String username, String password) throws SQLException {
 return determineTargetDataSource().getConnection(username, password);
 }

 protected DataSource determineTargetDataSource() {
 Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
  // 确定当前要使用的数据源
 Object lookupKey = determineCurrentLookupKey();
 DataSource dataSource = this.resolvedDataSources.get(lookupKey);
 if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
 dataSource = this.resolvedDefaultDataSource;
 }
 if (dataSource == null) {
 throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
 }
 return dataSource;
 }

 /**
 * Determine the current lookup key. This will typically be implemented to check a thread-bound transaction context.
 * <p>
 * Allows for arbitrary keys. The returned key needs to match the stored lookup key type, as resolved by the
 * {@link #resolveSpecifiedLookupKey} method.
 */
 @Nullable
 protected abstract Object determineCurrentLookupKey();

 // 省略相关代码...
}

重写 AbstractRoutingDataSource 的 determineCurrentLookupKey() 方法,可以实现对多数据源的支持

思路:

  • 重写其 determineCurrentLookupKey() 方法,支持选择不同的数据源
  • 初始化多个 DataSource 数据源到 AbstractRoutingDataSource 的 resolvedDataSources 属性中
  • 然后通过 Spring AOP, 以自定义注解作为切点,根据不同的数据源的 Key 值,设置当前线程使用的数据源

接下来的实现方式是 Spring Boot 结合 Druid 配置动态数据源

(一)引入依赖

基于 3.继承SpringBoot 中已添加的依赖再添加对AOP支持的依赖,如下:

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

(二)开始实现

1. DataSourceContextHolder

DataSourceContextHolder 使用 ThreadLocal 存储当前线程指定的数据源的 Key 值,代码如下:

package cn.tzh.mybatis.config;

import lombok.extern.slf4j.Slf4j;

import java.util.HashSet;
import java.util.Set;

/**
 * @author tzh
 * @date 2021/1/4 11:42
 */
@Slf4j
public class DataSourceContextHolder {

 /**
  * 线程本地变量
  */
 private static final ThreadLocal<String> DATASOURCE_KEY = new ThreadLocal<>();

 /**
  * 配置的所有数据源的 Key 值
  */
 public static Set<Object> ALL_DATASOURCE_KEY = new HashSet<>();

 /**
  * 设置当前线程的数据源的 Key
  *
  * @param dataSourceKey 数据源的 Key 值
  */
 public static void setDataSourceKey(String dataSourceKey) {
  if (ALL_DATASOURCE_KEY.contains(dataSourceKey)) {
   DATASOURCE_KEY.set(dataSourceKey);
  } else {
   log.warn("the datasource [{}] does not exist", dataSourceKey);
  }
 }

 /**
  * 获取当前线程的数据源的 Key 值
  *
  * @return 数据源的 Key 值
  */
 public static String getDataSourceKey() {
  return DATASOURCE_KEY.get();
 }

 /**
  * 移除当前线程持有的数据源的 Key 值
  */
 public static void clear() {
  DATASOURCE_KEY.remove();
 }
}

2. MultipleDataSource

重写其 AbstractRoutingDataSource 的 determineCurrentLookupKey() 方法,代码如下:

package cn.tzh.mybatis.config;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * @author tzh
 * @date 2021/1/4 11:44
 */
public class MultipleDataSource extends AbstractRoutingDataSource {

 /**
  * 返回当前线程是有的数据源的 Key
  *
  * @return dataSourceKey
  */
 @Override
 protected Object determineCurrentLookupKey() {
  return DataSourceContextHolder.getDataSourceKey();
 }
}

3. DataSourceAspect切面

使用 Spring AOP 功能,定义一个切面,用于设置当前需要使用的数据源,代码如下:

package cn.tzh.mybatis.config;

import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * @author tzh
 * @date 2021/1/4 11:46
 */
@Aspect
@Component
@Log4j2
public class DataSourceAspect {

 @Before("@annotation(cn.tzh.mybatis.config.TargetDataSource)")
 public void before(JoinPoint joinPoint) {
  MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
  Method method = methodSignature.getMethod();
  if (method.isAnnotationPresent(TargetDataSource.class)) {
   TargetDataSource targetDataSource = method.getAnnotation(TargetDataSource.class);
   DataSourceContextHolder.setDataSourceKey(targetDataSource.value());
   log.info("set the datasource of the current thread to [{}]", targetDataSource.value());
  } else if (joinPoint.getTarget().getClass().isAnnotationPresent(TargetDataSource.class)) {
   TargetDataSource targetDataSource = joinPoint.getTarget().getClass().getAnnotation(TargetDataSource.class);
   DataSourceContextHolder.setDataSourceKey(targetDataSource.value());
   log.info("set the datasource of the current thread to [{}]", targetDataSource.value());
  }
 }

 @After("@annotation(cn.tzh.mybatis.config.TargetDataSource)")
 public void after() {
  DataSourceContextHolder.clear();
  log.info("clear the datasource of the current thread");
 }
}

4. DruidConfig

Druid 配置类,代码如下:

package cn.tzh.mybatis.config;

import com.alibaba.druid.support.spring.stat.DruidStatInterceptor;
import org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author tzh
 * @date 2021/1/4 11:49
 */
@Configuration
public class DruidConfig {

 @Bean(value = "druid-stat-interceptor")
 public DruidStatInterceptor druidStatInterceptor() {
  return new DruidStatInterceptor();
 }

 @Bean
 public BeanNameAutoProxyCreator beanNameAutoProxyCreator() {
  BeanNameAutoProxyCreator beanNameAutoProxyCreator = new BeanNameAutoProxyCreator();
  beanNameAutoProxyCreator.setProxyTargetClass(true);
  // 设置要监控的bean的id
  beanNameAutoProxyCreator.setInterceptorNames("druid-stat-interceptor");
  return beanNameAutoProxyCreator;
 }
}

5. MultipleDataSourceConfig

MyBatis 的配置类,配置了 2 个数据源,代码如下:

package cn.tzh.mybatis.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.TypeHandler;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.boot.autoconfigure.ConfigurationCustomizer;
import org.mybatis.spring.boot.autoconfigure.MybatisProperties;
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ResourceLoader;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import javax.sql.DataSource;
import java.beans.FeatureDescriptor;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author tzh
 * @projectName code-demo
 * @title MultipleDataSourceConfig
 * @description
 * @date 2021/1/4 13:43
 */
@Configuration
@EnableConfigurationProperties({MybatisProperties.class})
public class MultipleDataSourceConfig {

 private final MybatisProperties properties;
 private final Interceptor[] interceptors;
 private final TypeHandler[] typeHandlers;
 private final LanguageDriver[] languageDrivers;
 private final ResourceLoader resourceLoader;
 private final DatabaseIdProvider databaseIdProvider;
 private final List<ConfigurationCustomizer> configurationCustomizers;

 public MultipleDataSourceConfig(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider, ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider, ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider, ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
  this.properties = properties;
  this.interceptors = (Interceptor[]) interceptorsProvider.getIfAvailable();
  this.typeHandlers = (TypeHandler[]) typeHandlersProvider.getIfAvailable();
  this.languageDrivers = (LanguageDriver[]) languageDriversProvider.getIfAvailable();
  this.resourceLoader = resourceLoader;
  this.databaseIdProvider = (DatabaseIdProvider) databaseIdProvider.getIfAvailable();
  this.configurationCustomizers = (List) configurationCustomizersProvider.getIfAvailable();
 }

 @Bean(name = "master", initMethod = "init", destroyMethod = "close")
 @ConfigurationProperties(prefix = "spring.datasource.druid.master")
 public DruidDataSource master() {
  return DruidDataSourceBuilder.create().build();
 }

 @Bean(name = "slave", initMethod = "init", destroyMethod = "close")
 @ConfigurationProperties(prefix = "spring.datasource.druid.slave")
 public DruidDataSource slave() {
  return DruidDataSourceBuilder.create().build();
 }

 @Bean(name = "dynamicDataSource")
 public DataSource dynamicDataSource() {
  MultipleDataSource dynamicRoutingDataSource = new MultipleDataSource();

  Map<Object, Object> dataSources = new HashMap<>();
  dataSources.put("master", master());
  dataSources.put("slave", slave());

  dynamicRoutingDataSource.setDefaultTargetDataSource(master());
  dynamicRoutingDataSource.setTargetDataSources(dataSources);

  DataSourceContextHolder.ALL_DATASOURCE_KEY.addAll(dataSources.keySet());

  return dynamicRoutingDataSource;
 }

 @Bean
 public SqlSessionFactory sqlSessionFactory() throws Exception {
  SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
  factory.setDataSource(dynamicDataSource());
  factory.setVfs(SpringBootVFS.class);
  if (StringUtils.hasText(this.properties.getConfigLocation())) {
   factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
  }

  this.applyConfiguration(factory);
  if (this.properties.getConfigurationProperties() != null) {
   factory.setConfigurationProperties(this.properties.getConfigurationProperties());
  }

  if (!ObjectUtils.isEmpty(this.interceptors)) {
   factory.setPlugins(this.interceptors);
  }

  if (this.databaseIdProvider != null) {
   factory.setDatabaseIdProvider(this.databaseIdProvider);
  }

  if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
   factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
  }

  if (this.properties.getTypeAliasesSuperType() != null) {
   factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
  }

  if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
   factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
  }

  if (!ObjectUtils.isEmpty(this.typeHandlers)) {
   factory.setTypeHandlers(this.typeHandlers);
  }

  if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
   factory.setMapperLocations(this.properties.resolveMapperLocations());
  }

  Set<String> factoryPropertyNames = (Set) Stream.of((new BeanWrapperImpl(SqlSessionFactoryBean.class)).getPropertyDescriptors()).map(FeatureDescriptor::getName).collect(Collectors.toSet());
  Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
  if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) {
   factory.setScriptingLanguageDrivers(this.languageDrivers);
   if (defaultLanguageDriver == null && this.languageDrivers.length == 1) {
    defaultLanguageDriver = this.languageDrivers[0].getClass();
   }
  }

  if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {
   factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);
  }

  return factory.getObject();
 }

 private void applyConfiguration(SqlSessionFactoryBean factory) {
  org.apache.ibatis.session.Configuration configuration = this.properties.getConfiguration();
  if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) {
   configuration = new org.apache.ibatis.session.Configuration();
  }

  if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) {
   Iterator var3 = this.configurationCustomizers.iterator();

   while (var3.hasNext()) {
    ConfigurationCustomizer customizer = (ConfigurationCustomizer) var3.next();
    customizer.customize(configuration);
   }
  }

  factory.setConfiguration(configuration);
 }

 @Bean
 public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
  ExecutorType executorType = this.properties.getExecutorType();
  return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);
 }

 @Bean
 public PlatformTransactionManager masterTransactionManager() {
  // 配置事务管理器
  return new DataSourceTransactionManager(dynamicDataSource());
 }
}

6. 添加配置

server:
 port: 9092
 servlet:
 context-path: /mybatis-springboot-demo
 tomcat:
 accept-count: 200
 min-spare-threads: 200
spring:
 application:
 name: mybatis-springboot-demo
 profiles:
 active: test
 servlet:
 multipart:
  max-file-size: 100MB
  max-request-size: 100MB
 datasource:
 type: com.alibaba.druid.pool.DruidDataSource
 druid:
  master:
  driver-class-name: com.mysql.cj.jdbc.Driver
  url: jdbc:mysql://127.0.0.1:3306/mybatis-demo
  username: root
  password: root
  initial-size: 5 # 初始化时建立物理连接的个数
  min-idle: 20 # 最小连接池数量
  max-active: 20 # 最大连接池数量
  slave:
  driver-class-name: com.mysql.cj.jdbc.Driver
  url: jdbc:mysql://127.0.0.1:3306/mybatis-demo1
  username: root
  password: root
  initial-size: 5 # 初始化时建立物理连接的个数
  min-idle: 20 # 最小连接池数量
  max-active: 20 # 最大连接池数量
mybatis:
 type-aliases-package: cn.tzh.mybatis.entity
 mapper-locations: classpath:cn/tzh/mybatis/mapper/*.xml
 config-location: classpath:mybatis-config.xml
pagehelper:
 helper-dialect: mysql
 reasonable: true # 分页合理化参数
 offset-as-page-num: true # 将 RowBounds 中的 offset 参数当成 pageNum 使用
 supportMethodsArguments: true # 支持通过 Mapper 接口参数来传递分页参数

其中分别定义了 master 和 slave 数据源的相关配置

这样一来,在 DataSourceAspect 切面中根据自定义注解,设置 DataSourceContextHolder 当前线程所使用的数据源的 Key 值,MultipleDataSource 动态数据源则会根据该值设置需要使用的数据源,完成了动态数据源的切换

7. 使用示例

在 Mapper 接口上面添加自定义注解 @TargetDataSource,如下:

package cn.tzh.mybatis.mapper;

import cn.tzh.mybatis.config.TargetDataSource;
import cn.tzh.mybatis.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

/**
 * @author tzh
 * @date 2020/12/28 14:29
 */
@Mapper
public interface UserMapper {

 User selectUserOne(@Param("id") Long id);

 @TargetDataSource("slave")
 User selectUserTwo(@Param("id") Long id);
}

总结

上面就如何配置动态数据源的实现方式仅提供一种思路,其中关于多事务方面并没有实现,采用 Spring 提供的事务管理器,如果同一个方法中使用了多个数据源,并不支持多事务的,需要自己去实现(笔者能力有限),可以整合JAT组件,参考:SpringBoot2 整合JTA组件多数据源事务管理

分布式事务解决方案推荐使用 Seata 分布式服务框架

到此这篇关于SpringBoot配置数据库密码加密的实现的文章就介绍到这了,更多相关SpringBoot 数据库密码加密内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • jasypt 集成SpringBoot 数据库密码加密操作

    昨天看到一片文章,说是某某旗下酒店数据库因为程序员不小心,把数据库明文密码上传到了GitHub上,导致酒店数据注册资料.入住信息,开房记录被下载倒卖的消息. 作为程序员,开发的时候为了简单,账户明明都设置很简单,基本上数据库密码都是明文的,没做什么操作,至少我待过的公司都是这样,无论是测试环境还是线上环境,想想,这个也是一大安全隐患,在此,趁现在不忙,做些基于springboot的数据库密码加密. 1.pom.xml添加jar包(不同jdk选择不同的版本): <!-- jdk8 版本 整合jas

  • SpringBoot项目application.yml文件数据库配置密码加密的方法

    在Spring boot开发中,需要在application.yml文件里配置数据库的连接信息,或者在启动时传入数据库密码,如果不加密,传明文,数据库就直接暴露了,相当于"裸奔"了,因此需要进行加密处理才行. 使用@SpringBootApplication注解启动的项目,只需增加maven依赖 我们对信息加解密是使用这个jar包的: 编写加解密测试类: package cn.linjk.ehome; import org.jasypt.encryption.pbe.StandardP

  • springboot 整合druid数据库密码加密功能的实现代码

    在之前给大家介绍过Springboot Druid 自定义加密数据库密码的几种方案,感兴趣的朋友可以点击查看下,今天通过本文给大家介绍springboot 整合druid数据库密码加密功能,具体内容如下所示: 1.依赖引入 <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1

  • SpringBoot配置文件中数据库密码加密两种方案(推荐)

    SpringBoot项目经常将连接数据库的密码明文放在配置文件里,安全性就比较低一些,尤其在一些企业对安全性要求很高,因此我们就考虑如何对密码进行加密. 介绍两种加密方式:jasypt 可加密配置文件中所有属性值; druid 自带了加解密,可对数据库密码进行加密. jasypt 加解密 jasypt 是一个简单易用的加解密Java库,可以快速集成到 Spring 项目中.可以快速集成到 Spring Boot 项目中,并提供了自动配置,使用非常简单. 步骤如下: 1)引入maven依赖 <de

  • SpringBoot中使用com.alibaba.druid.filter.config.ConfigTools对数据库密码加密的方法

    SpringBoot中使用com.alibaba.druid.filter.config.ConfigTools对数据库密码加密 1.在本地Maven仓库中打开Powershell2.输入命令,然后点击回车3.将生成公钥和加密的数据库密码配置到SpringBoot项目中的yml配置文件中druid的pom版本 1.在本地Maven仓库中打开Powershell 2.输入命令,然后点击回车 scotttiger为未加密的数据库密码 privateKey为生成的私钥 publicKey为生成的公钥

  • springboot对数据库密码加密的实现

    我是黑帽子K,话不多说直接上加密.如有不对,欢迎指正. 开发的同学们都知道,例如项目依赖的信息,数据库信息一般是保存在配置文件中,而且都是明文,因此需要进行加密处理,今天在这里介绍下jasypt集成springboot加密的配置. 首先,这些都是建立在你的springboot项目是能正常运行的前提下. 第一步:pom文件加入依赖,如图: 这里提供一个版本, <dependency> <groupId>com.github.ulisesbocchio</groupId>

  • SpringBoot配置数据库密码加密的实现

    你在使用 MyBatis 的过程中,是否有想过多个数据源应该如何配置,如何去实现?出于这个好奇心,我在 Druid Wiki 的数据库多数据源中知晓 Spring 提供了对多数据源的支持,基于 Spring 提供的 AbstractRoutingDataSource,可以自己实现数据源的切换. 一.配置动态数据源 下面就如何配置动态数据源提供一个简单的实现: org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource,

  • springboot配置数据库密码特殊字符报错的解决

    目录 配置数据库密码特殊字符报错 解决 yml文件中密码特殊字符引起启动报错 原因有两个 解决办法 配置数据库密码特殊字符报错 一般的springboot项目会有application.yml或者application.properties文件,开发中需要连接数据库时密码可能会有特殊字符,.properties文件不会报错,但是.yml文件会报错. 解决 yml中password对应的值用单引号引住('!@test')就可以了,如下 spring:     datasource:        

  • springboot数据库密码加密的配置方法

    前言 由于系统安全的考虑,配置文件中不能出现明文密码的问题,本文就给大家详细介绍下springboot配置数据库密码加密的方法,下面话不多说了,来一起看看详细的介绍吧 1.导入依赖 <dependency> <groupId>com.github.ulisesbocchio</groupId> <artifactId>jasypt-spring-boot-starter</artifactId> <version>2.1.2</

  • Springboot Druid 自定义加密数据库密码的几种方案

    前言 开发过程中,配置的数据库密码通常是明文形式,这样首先第一个安全性不好(相对来说),不符合一个开发规范(如项目中不能出现明文账号密码),其实就是当出现特殊需求时,比如要对非运维人员开方服务器部分权限,但是又涉及项目部署的目录时,容易泄漏数据库密码,虽然一般生产环境中,数据库往往放入内网,访问只能通过内网访问,但是不管怎么说账号密码直接让人知道总归不好,甚至有些项目需要部署到客户环境中,但是可能共用一个公共数据库(数据库只向指定服务器开放外网端口或组建内网环境),这样的情况下,如果数据库密码再

  • Springboot2 集成 druid 加密数据库密码的配置方法

    一:环境 springboot 2.x druid 1.1.21 二:druid加密数据库密码 本地下载druid-1.1.21.jar包,运行cmd,输入命令 java -cp jar包路径 com.alibaba.druid.filter.config.ConfigTools 数据库密码 java -cp druid-1.1.21.jar com.alibaba.druid.filter.config.ConfigTools 数据库密码 运行成功输出 privateKey:MIIBVAIBA

随机推荐