如何打造一个Dubbo网关--数据源

上篇说到的平台还有另外一个重要的职责:统一数据源管理。为什么统一管理呢?又要怎么统一管理呢?

  1. 这里所说的数据源是指需要通过密码登录的中间件,包括但不限于:mysql、redis、es、s3等
  2. 比如线上库不应该给予开发人员权限,但是代码里总是需要配置一个可以读写的账号,这个账号不可能随时变更,而且是由开发人员掌控的
  3. 即使通过Druid之类的连接池加密,加密后的密码和publickey总要存在于服务器上的某个地方:env、启动参数、配置文件
  4. 假设Druid加密可以解决大部分问题,还有redis、es、s3等等更多不支持加密的sdk
  5. 配置中心也是同理,即使可以在管理界面对开发人员隐藏某一部分配置,但明文总存在于服务器上的某个地方,而且很方便就能找到

本篇内容主要讲述数据源注入。以下代码片段大部分来自于plume

思路

下面说下大致思路,实际上我们使用的要比这个更复杂,开源版可以视为一个demo

  1. 平台的接口只允许同网段的服务调用,保证安全
  2. 通过接口或者其它手段将数据源信息存储到平台使用的数据库,表名system_${env},密钥和加密后的数据都存储在其中。当然密钥也可以分离存储
  3. 业务系统在启动时调用平台接口,获取到密钥和加密后的数据。可以接入一个更复杂的获取流程,这里只是思路
  4. 解密并将数据源注入到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
/**
* 获取存储使用的key,返回一个byte数组放入数据库
*/
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();
}

/**
* 将从数据库读出来的byte数组转换成KeysetHandle
*/
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获取数据源资源,最后才进行其它资源的配置。这点是通过AutoConfigureBeforeAutoConfigureAfter完成的,这两个注解可以调整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 {
/**
* 平台提供的dubbo服务
*/
@Reference
private InitialService sourceInit;

/**
* 密钥
*/
private byte[] enckey;

/**
* 加密后的数据,key时bean名
*/
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;
}

如何打造一个Dubbo网关--数据源
https://back.pub/post/hh-code-dubbo-gateway-7/
作者
Dash
发布于
2018年12月9日
许可协议