上篇说到的平台还有另外一个重要的职责:统一数据源管理。为什么统一管理呢?又要怎么统一管理呢?
- 这里所说的数据源是指需要通过密码登录的中间件,包括但不限于:mysql、redis、es、s3等
- 比如线上库不应该给予开发人员权限,但是代码里总是需要配置一个可以读写的账号,这个账号不可能随时变更,而且是由开发人员掌控的
- 即使通过Druid之类的连接池加密,加密后的密码和publickey总要存在于服务器上的某个地方:env、启动参数、配置文件
- 假设Druid加密可以解决大部分问题,还有redis、es、s3等等更多不支持加密的sdk
- 配置中心也是同理,即使可以在管理界面对开发人员隐藏某一部分配置,但明文总存在于服务器上的某个地方,而且很方便就能找到
本篇内容主要讲述数据源注入。以下代码片段大部分来自于plume
思路
下面说下大致思路,实际上我们使用的要比这个更复杂,开源版可以视为一个demo
- 平台的接口只允许同网段的服务调用,保证安全
- 通过接口或者其它手段将数据源信息存储到平台使用的数据库,表名
system_${env}
,密钥和加密后的数据都存储在其中。当然密钥也可以分离存储
- 业务系统在启动时调用平台接口,获取到密钥和加密后的数据。可以接入一个更复杂的获取流程,这里只是思路
- 解密并将数据源注入到Spring容器内,开源版本中只包含mysql和redis
加密数据
在这里使用了Google tink
库里的HybridEncrypt
,加解密都需要一个KeysetHandle
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
|
private static byte[] storeKey(KeysetHandle keysetHandle) { final ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { CleartextKeysetHandle.write(keysetHandle, BinaryKeysetWriter.withOutputStream(bos)); } catch (IOException e) { log.warn("twins store key failed: ", e); throw new SecurityException(e.getMessage()); } return bos.toByteArray(); }
private static KeysetHandle readKey(byte[] pwd) { try { return CleartextKeysetHandle.read(BinaryKeysetReader.withBytes(pwd)); } catch (GeneralSecurityException | IOException e) { log.warn("twins read key failed", e); throw new SecurityException("读取密钥失败"); } }
public static byte[] encrypt(byte[] input, byte[] pwd) { final KeysetHandle handle = readKey(pwd); try { final HybridEncrypt primitive = HybridEncryptFactory.getPrimitive(handle.getPublicKeysetHandle()); return primitive.encrypt(input, associate); } catch (GeneralSecurityException e) { log.warn("twins decrypt failed", e); throw new SecurityException("读取密钥失败"); } }
public static byte[] decrypt(byte[] input, byte[] pwd) { final KeysetHandle handle = readKey(pwd); try { final HybridDecrypt primitive = HybridDecryptFactory.getPrimitive(handle); return primitive.decrypt(input, associate); } catch (GeneralSecurityException e) { log.warn("twins decrypt failed", e); throw new SecurityException("读取密钥失败"); } }
|
因为整个库的封装已经很完善了,这里就只是简单的调用
业务请求
业务系统启动时会优先把Dubbo启动起来,之后经由Dubbo的rpc获取数据源资源,最后才进行其它资源的配置。这点是通过AutoConfigureBefore
和AutoConfigureAfter
完成的,这两个注解可以调整Configuration
的初始化顺序
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 32 33 34 35 36 37 38
| @Configuration @AutoConfigureAfter(InitDefault.class) @ConditionalOnProperty(name = "source.init.enable", matchIfMissing = true, havingValue = "true") public class InitSource {
@Reference private InitialService sourceInit;
private byte[] enckey;
private Map<String, KeystoreResult> keystore;
@PostConstruct private void init() { final PairResult<byte[], byte[]> source = sourceInit.getSource(new GroupEntry( PlatformConstants.APPID, PlatformConstants.APPNAME, StartupConstants.RUN_MODE, PlatformConstants.GROUP, "")); if (null != source && source.isSuccess()) { enckey = source.getFirst(); keystore = KryoBaseUtil.readFromByteArray(source.getLast()); log.info("[{}] 获取初始化资源成功", PlatformConstants.APPNAME); } else { log.error("[{}] 获取初始化资源失败: {}", PlatformConstants.APPNAME, source); throw new InnerException("初始化资源失败"); } }
...... }
|
这里为了简单是把密钥和密文分开为两个byte数组,但实际使用时这两个应该在一个byte数组中;约定密钥从第几位开始读,或者按照规则去分离密钥和密文才是正确且较为安全的
数据源注入
直接通过@Bean
注入,或者通过BeanFactory
都可以
通过@Bean
注入比较简单,但只适合有一个的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Bean public RedisConnectionFactory redisFactory() { try { final KeystoreResult keystore = this.keystore.get(PlatformConstants.SOURCE_REDIS); if (null == keystore) { throw new InnerException("初始化资源失败"); } return SourceGet.getLettuceFactory(Twins.decrypt(keystore.getUrls(), enckey), Twins.decrypt(keystore.getPassword(), enckey)); } catch (Exception e) { log.error("[{}] REDIS初始化失败: {}, {}", PlatformConstants.APPNAME, e.getMessage(), e); throw new InnerException("初始化资源失败"); } }
|
通过BeanFactory
可以获得更大的自由度,必须拿到DefaultListableBeanFactory
,一般情况下强转即可
1 2 3 4 5 6 7 8 9
| this.factory.registerBeanDefinition(name, beanDefinition(client, true));
private <T> BeanDefinition beanDefinition(T source, boolean primary) { final RootBeanDefinition definition = new RootBeanDefinition(source.getClass()); definition.setScope(SCOPE_SINGLETON); definition.setInstanceSupplier(() -> source); definition.setPrimary(primary); return definition; }
|