Dubbo从2.7.5开始支持TLS,但是有关于这方面的资料非常少,官方文档也非常简单。所以专门记录下来自己的踩坑之路,如果对你有所帮忙,麻烦老板给我点个赞!

前置知识

在使用Dubbo TLS 协议之前,最好先了解一下TLS相关的知识,主要包括两方面:

  1. 各种证书相关
  2. TLS认证流程

以下内容是看了各类资料后自己的理解,有不对的还请指出!

证书相关概念

  1. CA:颁发证书的权威机构
  2. 根证书:暂时就叫CA证书吧,由CA颁发,即这个证书代表了某一方CA自己,因为CA是权威的,所以等价于该CA证书也是可信任的,浏览器里已经默认嵌入了一些权威机构的根证书
  3. 数字证书:基于根证书签发的证书(我们可以理解为二级证书或下一层证书)。如果站点是对外使用,这个证书一般需我们去买购买
  4. 自签发证书:自己签发给自己的证书,此时该证书的签发者和被签发者相同,比如根证书

证书内容

一个证书应该包括哪些内容?

  1. 该证书对应的公钥:每个证书都需要有一对公私钥,公钥作为证书里面的内容,对外公开,私钥自己保存
  2. 签发者:是谁签发的。对于根证书而言,签发者和被签发者都是它自己
  3. 被签发者:签发给谁
  4. 有效期
  5. 其它信息

证书签发

如何签发一个证书?

  1. 首先基于某种非对称加密算法生成一对公私
  2. 基于某种hash算法对证书明文进行hash计算, 得到 H1,
  3. 使用签发者的私钥对 H1 进行加密,得到 P1 , 这个步骤称为签名
  4. 证书的明文 + P1 就是证书的完整内容。这个数字证书里面包括了: 证书基本信息、该证书对应的公钥、签名信息

证书校验

浏览器收到一个证书,该如何校验这是不是一个有效的证书?

  1. 基于某种hash算法对证书明文进行hash计算, 得到 H2
  2. 使用签发者的公钥对 该证书的签名信息进行解密,得到 H3 。 浏览器为啥有签发者的公钥? 前面不是说了浏览器内嵌了一些CA根证书吗?还有一种情况是自己手动添加的
  3. 判断 H2 和 H3 是否相同。如果相同,说明证书内容没有被串改,因为签名信息你改不了,签发者的私钥只有CA权威机构自己才有

TLS交互流程

这里说的是单向认证,即浏览器要确保访问的站点是安全的,即 浏览器要认证server端,这主要是保护浏览器一端

  1. 浏览器访问某站点,该站点返回浏览器一个数字证书
  2. 浏览器对这个数字证书进行校验
  3. 校验通过后,浏览器生成一个随即数,然后使用该数字证书的公钥对其加密,返回给server端
  4. Server收到请求后用证书的私钥随机数进行解密
  5. 后续两者就用这个随机数作为对称加密的密钥来加密传输的数据

整合过程

证书准备

假设在开始之前,我们已经准备好了如下证书:

  1. CA证书:ca.crt
  2. 数字证书对1:servcer.crt server.key
  3. 数字证书对2:client.crt client.key

单向认证

即只要认证一方,比如在请求之前,consumer要认证provider 或者 provider要认证consumer,此时就是单向认证

被认证方

即谁需要被认证,在dubbo中指 consumerprovider。在单向认证中,被认证方需要提供数字证书该数字证书对应的密钥。这里用的是server密钥对,其实用client密钥对也是一样的

  1. // 首先通过ProtocolConfig开启TLS协议
  2. @Bean
  3. public ProtocolConfig protocolConfig() {
  4. ProtocolConfig protocolConfig = new ProtocolConfig();
  5. protocolConfig.setName("dubbo");
  6. protocolConfig.setPort(7798);
  7. protocolConfig.setThreadpool("fixed");
  8. protocolConfig.setThreads(200);
  9. protocolConfig.setDispatcher("message");
  10. protocolConfig.setSslEnabled(true);
  11. return protocolConfig;
  12. }
  13. @Bean
  14. public SslConfig sslConfig() {
  15. SslConfig sslConfig = new SslConfig();
  16. // 如果被认证方是provider, 设置下面两个
  17. sslConfig.setServerKeyCertChainPath("classpath:certs/servcer.crt");
  18. sslConfig.setServerPrivateKeyPath("classpath:certs/servcer.key");
  19. // 如果被认证方是consumer, 设置下面两个
  20. sslConfig.setClientKeyCertChainPath("classpath:certs/servcer.crt");
  21. sslConfig.setClientPrivateKeyPath("classpath:certs/servcer.key");
  22. return sslConfig;
  23. }
  24. 复制代码

认证方

即谁需要被认证,在dubbo中指 consumerprovider。在单向认证中,认证方需要提供被认证方对应的证书或者ca证书

  1. @Bean
  2. public SslConfig sslConfig() {
  3. SslConfig sslConfig = new SslConfig();
  4. // 如果认证方是consumer, 设置下面这个
  5. sslConfig.setClientTrustCertCollectionPath("classpath:certs/servcer.crt");
  6. // 如果认证方是provider, 设置下面这个
  7. sslConfig.setServerTrustCertCollectionPath("classpath:certs/servcer.crt");
  8. return sslConfig;
  9. }
  10. 复制代码

双向认证

provider

  1. // 首先通过ProtocolConfig开启TLS协议
  2. @Bean
  3. public ProtocolConfig protocolConfig() {
  4. ProtocolConfig protocolConfig = new ProtocolConfig();
  5. ......
  6. protocolConfig.setSslEnabled(true);
  7. return protocolConfig;
  8. }
  9. @Bean
  10. public SslConfig sslConfig() {
  11. SslConfig sslConfig = new SslConfig();
  12. sslConfig.setServerKeyCertChainPath("classpath:certs/servcer.crt");
  13. sslConfig.setServerPrivateKeyPath("classpath:certs/servcer.crt");
  14. // 下面代表双向认证 这里代表服务端也要认证消费端
  15. sslConfig.setServerTrustCertCollectionPath("classpath:certs/ca.crt");
  16. return sslConfig;
  17. }
  18. 复制代码

consumer

  1. // 首先通过ProtocolConfig开启TLS协议
  2. @Bean
  3. public ProtocolConfig protocolConfig() {
  4. ProtocolConfig protocolConfig = new ProtocolConfig();
  5. ......
  6. protocolConfig.setSslEnabled(true);
  7. return protocolConfig;
  8. }
  9. @Bean
  10. public SslConfig sslConfig() {
  11. SslConfig sslConfig = new SslConfig();
  12. sslConfig.setClientKeyCertChainPath("classpath:certs/client.crt");
  13. sslConfig.setClientPrivateKeyPath("classpath:certs/client.crt");
  14. // 下面代表双向认证 这里代表服务端也要认证消费端
  15. sslConfig.setClientTrustCertCollectionPath("classpath:certs/ca.crt");
  16. return sslConfig;
  17. }
  18. 复制代码

案例分析

  1. Dubbo版本: 2.7.8
  2. 确认证书没有问题
  3. 在本地新建了两个Dubbo客户端,单向认证、双向认证 都可以测试通过;将这两个客户端部署到K8s集群之后,TLS认证失败。此时本地和开发环境的配置项都是从开发环境的apollo配置中心拉取的,按理应该是一样的

问题重现

  1. 本地开启TLS测试, 不论单向 双向认证 都可以正常访问
  2. 将这两个应用部署到K8s之后,观察启动日志: client 先和 server 建立连接,然后马上断开连接, 同时server端报 shakehand time out 异常,但是马上 client 和 server 又建立连接,一直重复这个动作
  3. 本地启动 server 和 client ,通过抓包工具wireshark观察 server端口,确认走了TLS认证
  4. 从K8s上下载对应的starter.jar, 启动并通过抓包工具wireshark观察,发现根本没走TLS认证。我就纳闷了,难道是打包有问题吗?或者就是配置问题,此时本地和开发环境的配置项都是从开发环境的apollo配置中心拉取的,按理应该是一样的
  5. 重新弄了两个新的Dubbo客户端,还是遇到相同的问题

问题原因

最后发现,本地的应用连的是本地的ZK;K8s上的应用,连的是开发环境的ZK。开发环境开启了Dubbo的全局配置,配置项存放在ZK中,这里面的配置项会对每个客户端生效,并且全局配置中开启了简化URL。 简化URL开启之后,导致TLS相关的配置在应用启动时无法获取,从而导致TLS不生效,而本地ZK没有开启dubbo全局配置,所以不会有影响。

开启简化URL: 可以通过Dubbo全局化配置开启,此时所有Dubbo客户端都生效;也可以在某个客户端单独开启,此时只有这个客户端生效。相关配置:dubbo.registry.simplified=true

解决方案

Dubbo提供了一个配置项,在开启简化URL的情况下,可以配置一些属性,让其不被简化,如下:

  1. dubbo.registry.extra-keys=ssl-enabled
  2. 复制代码

因为我们已经开启了Dubbo全局化配置,所以在将配置项添加到全局化配置项中,此时所有Dubbo客户端就都可以生效了。