引言
前一篇博文,已经完整的介绍了数据库脱敏的场景及方案,来自京东数科的Sharding-JDBC开源项目通过对数据源中间代理的方式透明化的实现了这个功能,但是,功能虽然实现了,sql兼容的小问题还是很多,比如目前不支持子查询,数据库定义的关键字不允许使用,等等问题,反观我们需要加解密的字段,其实相比业务的sql来说占比非常小,即使遇到了和组件不兼容的地方也可以稍加改动解决掉,所以最后博主给出了一个比较完善的组件集成方案:多数据源模式,需要加解密的数据源和业务其他数据源隔离。即解决了数据库字段加解密的问题,同时也解决了组件对sql的兼容问题。下面是具体的集成步骤以及需要注意的点
引入依赖
1 <dependency> 2 <groupId>org.apache.shardingsphere</groupId> 3 <artifactId>sharding-jdbc-spring-boot-starter</artifactId> 4 <version>${sharding.jdbc.version}</version> 5 </dependency>
这里需要说明下,虽然采用多数据源兼容后,不能使用组件基于spring boot自动装配功能,但是这里还是建议导入sharding-spring-boot-starter包,因为这个包下内置了配置映射的类,在自定义数据源的时候非常有用
添加sharding数据源配置
1#数据库源配置 2spring.shardingsphere.datasource.name = ds 3 4spring.shardingsphere.datasource.ds.type = com.zaxxer.hikari.HikariDataSource 5spring.shardingsphere.datasource.ds.driver-class-name = com.mysql.jdbc.Driver 6spring.shardingsphere.datasource.ds.jdbc-url = jdbc:mysql://xxx?autoReconnect=true&useUnicode=true&characterEncoding=utf-8 7spring.shardingsphere.datasource.ds.username = root 8spring.shardingsphere.datasource.ds.password = xxx 9 10spring.shardingsphere.encrypt.encryptors.encryptor_aes.type = aes 11spring.shardingsphere.encrypt.encryptors.encryptor_aes.props.aes.key.value = 123456 12spring.shardingsphere.encrypt.tables.account.columns.password.plainColumn = password 13spring.shardingsphere.encrypt.tables.account.columns.password.cipherColumn = password_encrypt 14spring.shardingsphere.encrypt.tables.account.columns.password.encryptor = encryptor_aes 15 16spring.shardingsphere.props.sql.show = true 17spring.shardingsphere.props.query.with.cipher.column = true
排除自动装配
@SpringBootApplication(exclude = SpringBootConfiguration.class)
由于导入了starter包,所以这里需要手动排除自动装载类,
业务数据源配置
多数据源后,业务本身的数据源也需要手动配置,默认的spring boot jpa自动转载类会判断上线文中是否存在EntityManagerFactory类,如果有就不会初始化了,所以两个数据源都需要手动配置
1@Configuration 2@EnableConfigurationProperties(JpaProperties.class) 3public class DataSourceConfiguration{ 4 5 private final JpaProperties jpaProperties; 6 private final Environment environment; 7 8 public DataSourceConfiguration(JpaProperties jpaProperties, Environment environment) { 9 this.jpaProperties = jpaProperties; 10 this.environment = environment; 11 } 12 13 @Primary 14 @Bean 15 public DataSource dataSource(){ 16 String prefix = "spring.shardingsphere.datasource."; 17 String each = getDataSourceNames(prefix).get(0); 18 try { 19 return getDataSource(prefix, each); 20 } catch (final ReflectiveOperationException ex) { 21 throw new ShardingSphereException("Can't find datasource type!", ex); 22 } 23 } 24 25 @Primary 26 @Bean 27 public EntityManagerFactory entityManagerFactory() { 28 HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); 29 vendorAdapter.setDatabase(Database.MYSQL); 30 vendorAdapter.setGenerateDdl(true); 31 vendorAdapter.setShowSql(true); 32 LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); 33 factory.setJpaVendorAdapter(vendorAdapter); 34 factory.setPersistenceUnitName("default"); 35 factory.setPackagesToScan(Constants.BASE_PACKAGES); 36 factory.setDataSource(dataSource()); 37 factory.setJpaPropertyMap(jpaProperties.getProperties()); 38 factory.afterPropertiesSet(); 39 return factory.getObject(); 40 } 41 42 @Bean 43 @Primary 44 public EntityManager entityManager(EntityManagerFactory entityManagerFactory){ 45 return SharedEntityManagerCreator.createSharedEntityManager(entityManagerFactory); 46 } 47 48 @Primary 49 @Bean 50 public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){ 51 JpaTransactionManager txManager = new JpaTransactionManager(); 52 txManager.setEntityManagerFactory(entityManagerFactory); 53 return txManager; 54 } 55 56 private ListgetDataSourceNames(final String prefix) { 57 StandardEnvironment standardEnv = (StandardEnvironment) environment; 58 standardEnv.setIgnoreUnresolvableNestedPlaceholders(true); 59 return null == standardEnv.getProperty(prefix + "name") 60 ? new InlineExpressionParser(standardEnv.getProperty(prefix + "names")).splitAndEvaluate() : Collections.singletonList(standardEnv.getProperty(prefix + "name")); 61 } 62 63 @SuppressWarnings("unchecked") 64 private DataSource getDataSource(final String prefix, final String dataSourceName) throws ReflectiveOperationException { 65 Map dataSourceProps = PropertyUtil.handle(environment, prefix + dataSourceName.trim(), Map.class); 66 Preconditions.checkState(!dataSourceProps.isEmpty(), "Wrong datasource properties!"); 67 DataSource result = DataSourceUtil.getDataSource(dataSourceProps.get("type").toString(), dataSourceProps); 68 DataSourcePropertiesSetterHolder.getDataSourcePropertiesSetterByType(dataSourceProps.get("type").toString()).ifPresent( 69 dataSourcePropertiesSetter -> dataSourcePropertiesSetter.propertiesSet(environment, prefix, dataSourceName, result)); 70 return result; 71 } 72}
上面代码需要注意三个地方,一是数据源的配置,是以sharding的配置来解析获得的,是因为我们已经集成过了,不想改动配置,所以如此,如果还没集成过,可以直接使用spring 配置数据源的方式配置即可。二是EntityManager的初始化,通过SharedEntityManagerCreator包装了下,是因为我们业务的查询通过继承SimpleJpaRepository来扩展功能的,通过SharedEntityManagerCreator包装保留了SimpleJpaRepository内部定义的事务功能。三是需要给所有的业务数据源的配置添加 @Primary注解,让sprign上下文默认使用业务数据源
加解密数据源配置
1/** 2 * @author: kl @kailing.pub 3 * @date: 2020/5/18 4 */ 5@Configuration 6@EnableConfigurationProperties({JpaProperties.class,SpringBootEncryptRuleConfigurationProperties.class, SpringBootPropertiesConfigurationProperties.class}) 7@AutoConfigureAfter({DataSourceAutoConfiguration.class, DataSourceConfiguration.class}) 8public class EncryptDataSourceConfiguration { 9 10 private final SpringBootPropertiesConfigurationProperties props; 11 private final SpringBootEncryptRuleConfigurationProperties encryptRule; 12 private final JpaProperties jpaProperties; 13 private final DataSource dataSource; 14 15 public EncryptDataSourceConfiguration(SpringBootPropertiesConfigurationProperties props, SpringBootEncryptRuleConfigurationProperties encryptRule, JpaProperties jpaProperties, DataSource dataSource) { 16 this.props = props; 17 this.encryptRule = encryptRule; 18 this.jpaProperties = jpaProperties; 19 this.dataSource = dataSource; 20 } 21 22 @Bean 23 public DataSource encryptDataSource() throws SQLException { 24 return EncryptDataSourceFactory.createDataSource(dataSource, new EncryptRuleConfigurationYamlSwapper().swap(encryptRule), props.getProps()); 25 } 26 27 @Bean 28 public EntityManagerFactory encryptEntityManagerFactory() throws SQLException { 29 HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); 30 vendorAdapter.setDatabase(Database.MYSQL); 31 LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean(); 32 factory.setJpaVendorAdapter(vendorAdapter); 33 factory.setPersistenceUnitName("encryptPersistenceUnit"); 34 factory.setPackagesToScan(Constants.BASE_PACKAGES); 35 factory.setDataSource(encryptDataSource()); 36 factory.setJpaPropertyMap(jpaProperties.getProperties()); 37 factory.afterPropertiesSet(); 38 return factory.getObject(); 39 } 40 41 @Bean 42 public EntityManager encryptEntityManager() throws SQLException { 43 return SharedEntityManagerCreator.createSharedEntityManager(encryptEntityManagerFactory()); 44 } 45 46 @Bean 47 public PlatformTransactionManager encryptTransactionManager() throws SQLException { 48 JpaTransactionManager txManager = new JpaTransactionManager(); 49 txManager.setEntityManagerFactory(encryptEntityManagerFactory()); 50 return txManager; 51 } 52}
加解密数据源的源来自于业务数据源,只是在这里给业务数据源又代理了一层加解密的逻辑。加解密的规则配置采用了sharding-spring-boot-starter包中的映射类,所以可以保留和spring boot配置方式一致。
加解密数据源的使用
在使用时,因为默认使用的是业务数据源,所以需要在需要加解密的地方通过@Qualifier("encryptEntityManager")显示的注入加解密的数据源代理,如:
1@Repository 2public class AccountRepository extends AbstractJpaRepository { 3 4 public AccountRepository(@Qualifier("encryptEntityManager") EntityManager em) { 5 super(AccountModel.class, em); 6 } 7 8 @Override 9 @Transactional(transactionManager = "encryptTransactionManager") 10 public S save(S entity) { 11 return super.save(entity); 12 } 13}
另,需要手动指定加解密数据源的事务管理器。
如果你使用了JpaRepository等接口,在启用@EnableJpaRepositories的注解里,默认装载的业务数据源配置的EntityManagerFactory实例,需要加解密时,要设置如下参数:
@EnableJpaRepositories(basePackages = "com.xxx",entityManagerFactoryRef = "encryptEntityManagerFactory",transactionManagerRef = "encryptTransactionManager")
结语
没有十全十美的组件,Sharding-JDBC的数据脱敏方案已经趋向于完美了。由于组件本身的架构设计,确实不好做到100%的兼容。在发现加解密组件不支持子查询时,博主发现实现这个功能很简单,尝试过向官方提交这个功能的pr。经过对组件的进一步了解发现,从全局考虑实现这个功能非常复杂,也就放弃了,熟悉java的DBA有志之士可以去试试。目前这个多数据源模式可以很好的解决了sql兼容问题,如果有更好的集成方案,欢迎在下面留言交流
作者简介:
陈凯玲,2016年5月加入凯京科技。负责基础架构中间件的迭代,救火队队长。独立博客KL博客(http://www.kailing.pub)博主。