Skip to content

19 安全:身份认证、资源鉴权和加密传输都是怎么实现的?

你好,我是文强。

近几年业界的安全问题频繁发生,系统数据的安全性也越来越受到重视。作为消息队列的主要使用者,我经常有这么一个疑问:消息队列是如何保证数据安全的?想必你在使用消息队列产品的时候,应该也有同样的疑问。

在我看来,从消息队列架构全流程拆解的角度,消息队列的系统安全由六部分组成: 网络隔离传输安全集群认证资源授权自我保护数据加密

今天我们先来聊一聊网络隔离、传输安全、集群认证、资源授权四个部分,整体了解一下集群的安全控制,思考如何在传输过程、访问控制两方面保证数据安全。下一节再讲自我保护和数据加密。

网络隔离的安全性

关于网络隔离,你可能听过一个观点:不管是什么系统,从安全的角度来看,最完美的保护就是网络隔离。这很好理解,一个完全隔离封闭的网络,是不会存在网络安全问题的,因为别人根本无法访问它。

在云服务中,虚拟网络(VPC)就是一个独立的子网。如果不做对外打通(比如开通公网、跟其他网络拉专线),它的数据在这个网络内就是安全的。

虚拟网络只是独立网络的一种叫法,你的公司可能有内网的概念,它就是独立网络的一种。

但是,在实际工业环境中,除非是一些特殊的银行、国企机构的私有云环境会有完整的网络隔离,大部分情况下,我们的系统是需要和外部服务进行交互的。比如外网或者其他子网的服务需要访问我们的消息队列,或者内网应用需要外网的某些服务,此时就需要进行打通,网络隔离就无法起作用了。

在网络必须打通的情况下,如果你对安全的要求不是特别高,按默认的方式使用消息队列就可以了,因为几乎所有的消息队列默认都是不开启认证鉴权的,比如Kafka、RocketMQ、Pulsar、RabbitMQ默认都可以直接访问的。

如果你的内网中的各个服务需要进行访问控制,比如你公司的支付团队的机密数据是不能让其他团队访问到的,就需要后续认证和鉴权机制的存在,我们稍后会讲到。

数据传输过程加密

我们的应用必须访问外界服务,也必定存在公网的数据传输。为了防止数据在传输过程中不被窃取、改变,确保数据的完整性,我们需要对传输过程中的数据进行加密。

从技术的角度来看,数据传输安全的核心是SSL/TLS,你可以简单理解成,如果要保证传输过程中的数据安全,就要用SSL/TLS。消息队列也是这个逻辑, 几乎所有的消息队列产品 传输过程中的加密机制都是基于SSL/TLS实现的,你可以在它们的官方文档找到相应的资料,比如支持TLS的 Pulsar 或者 RabbitMQ,支持 SSL的 Kafka

那你可能会疑问了,SSL/TLS是什么意思?有什么区别吗?为什么不同的MQ支持不同?

我们第一次接触这两个词都有同样的疑问。简单理解, SSL和TLS是同一个东西。SSL 3.0及之前的版本叫SSL,3.0之后叫做TLS,TLS是SSL的升级版。

所以你会发现,有些消息队列官网说支持SSL,有些支持TLS,一部分原因是各个产品安全模块开发的时间不一样,所以支持的SSL协议版本不一样,比如TLS 1.3是2018年才发表的。但从应用角度看,它俩的区别并不大,比如证书制作步骤、代码库的调用方法都差不多。

前面我们讲过,大部分消息队列为了保证延时和吞吐,都是基于四层的TCP协议构建的,所以在加密传输的实现上都是TCP + SSL/TLS。另外在七层的加密传输中,HTTPS 是我们最熟悉的一种,它的底层机制是基于HTTP + SSL实现的。

从技术上看,传输加密由两部分组成,服务端代码开启支持SSL和客户端配置SSL访问。

服务端开启SSL分为两步,首先需要制作SSL证书( 证书制作参考),然后使用对应语言的SSL库在服务端支持SSL协议,比如使用Java Netty 开发支持SSL的Server( Java Netty 集成SSL参考)。

客户端连接时,通过消息队列SDK集成的SSL功能,携带对应的公钥和证书信息,与服务端进行通信( Java Netty 客户端集成SSL参考)。

连接建立时的身份认证

加密传输,只能解决数据在网络传输过程中的安全性,此时消息队列集群资源还处于一个门户大开的状态,只要网络能通,集群就能被直接连接访问。为了解决这个问题,就需要开启集群认证。

集群认证 通俗解释就是消息队列中的登录功能。 前面提到,默认情况下消息队列是不开启认证的,需要时,我们可以通过配置开启认证。认证是客户端连接服务端的第一道门槛,主要是解决客户端连接到服务端时,是否允许建立连接的问题。

抽象来看,认证就是客户端携带认证信息(比如用户名+密码、Token、Auth等)连接上服务端。服务端首先会判断当前开启或配置的认证类型,然后校验这些信息,如果通过,就允许建立连接,否则,就返回认证错误。

不过,消息队列的身份认证真的这么简单吗?为什么你看到的消息队列的身份认证看起来那么复杂呢?可能有好多概念,比如SASL、OAuth、SCRAM、PLAINTEXT、Keberos、MTLS、JWT等等。

要了解这些,我们需要先来看一下身份认证的原始需求是什么,它其实包含两个方面: 完成身份认证支持多种认证方式

所以从代码实现上来看,为了支持多种认证方式,一般会包含认证框架和具体认证实现两部分。认证框架负责制定认证的规则和实现机制,多种具体的认证机制就基于框架制定的规则和机制来实现不同的认证方式。

框架和实现

最简单的认证框架,可以是一个Java 的接口定义。具体代码也很简单,先定义一个接口AuthenticationProvider,接口中包含了authenticate方法,只要实现了这个接口的类都是认证实现类,都可以执行认证操作。

public interface AuthenticationProvider extends Closeable {
  @Deprecated
  default String authenticate(AuthenticationDataSource authData)
  throws AuthenticationException {
     throw new AuthenticationException("Not supported");
  }
}

顺着这个思路,我们来看看当前主流的消息队列支持的认证方式,分析一下实现思路上是否是类似的。

身份认证框架

先来看一下各个主流消息队列和认证方式,我整理成了表格,看起来包含了好多专业术语,别担心,其实不复杂,当我们拆解了这些术语的含义,你会发现跟我们平时使用用户名密码登录后台系统没什么区别。

先来看 Kafka,Kafka的每种认证方式上都包含了SASL,那什么是SASL呢?

SASL的全称是Simple Authentication and Security Layer,翻译过来就是 简单身份验证和安全层,你可以把它理解为一个框架,在这个框架上扩展各种身份验证提供程序就可以了。

所以,Kafka 在开发的时候引入了SASL,然后基于SASL实现各种认证插件,比如GSSAPI、PLAIN、SCRAM、OAUTH等等,程序上就可以顺利集成各种认证机制了。

所以 Kafka 从源码上看就是基于 SASL 框架去实现各种认证机制的。Java Sever集成SASL的实现可以参考 Oracle 官方文档

RabbitMQ的实现机制和Pulsar类似。都是在内核提供了一个自定义实现的、可插入的身份验证框架,基于认证接口实现各种认证机制,并在配置文件中指定要启用的认证插件和参数,然后开启认证的。

Kafka和Pulsar /RabbitMQ 最大的区别在于认证框架的选择。 SASL框架的机制更完善,基于SASL需要遵循编码规范和机制,相对复杂,同时功能也相对较强,这是可预期的。

但是从消息队列的角度来看,一般自定义的身份认证框架基本可以满足认证需求。而且基于自定义的机制,实现起来会比较简单,编码成本较低。所以,从我的角度来看,自定义的身份认证框架就够了。

身份认证实现

有了认证框架,那怎样实现身份认证呢?

最简单的认证实现就是用户名+密码,把用户名和密码传递给服务端进行比对。不过为了安全性和各个场景下都能方便认证,业界提供了非常多种的认证机制,比如OAuth、Token、Kerberos、PLAINTEXT等等。

我介绍几种主流的认证机制,虽然具体实现方式不一样,但宏观思路基本是一致的。

  • 用户名+密码的机制

Kafka的PLAIN、SCRAM,RabbitMQ的PLAIN、AMQPPLAIN,RocketMQ的AccessKey 和 SecretKey都属于这类,用户名+密码完成身份认证,区别在于底层的实现不一样。

比如Kafka的PLAIN,是把用户账户文件配置到一个静态文件中,每次想要添加新的账户,都需要重启Kafka去加载静态文件,才能使之生效,十分不方便。SCRAM就在这个基础上升级,将用户名和密码存在了ZooKeeper上,可动态变更用户名和密码。以此类推。

  • Kerberos

Kerberos是一种计算机网络授权协议,用来在非安全网络中对个人通信进行身份认证,属于一种标准的授权协议。很多组件中都会支持它,所以在很多消息队列产品或者大数据产品的认证模块中,我们都会经常看到。

使用Kerberos的时候,一般需要先配置一个Kerberos配置中心,然后消息队列配置上配置中心的相关信息,收到客户端的验证请求的时候,通过Kerberos配置中心完成认证。

  • OAuth认证

你应该非常熟悉,在Web开发中用得很多。它的授权过程简单理解就是获取令牌(Token)的过程。它允许用户以Token的形式,授权第三方应用访问他们存储在另外服务提供者上的信息。

其他的比如JWT、原始Token授权、mTLS等等,原理是类似的,区别是不同厂商推出的满足不同特定的场景下的认证机制。

集群资源的访问控制

身份认证解决的是“是否允许连接建立”,回答“你是谁”的问题。那建立连接后是否就能访问所有的资源了呢?

答案肯定是不行的。因为在很多场景下,比如一个公司规模不大的时候,整个公司会共用一套集群。此时如果一些部门的数据不能让其他部门看到,身份认证就不能满足需求了。

此时就需要访问控制机制起作用了。

数据类和资源类操作控制

如果你运营过消息队列的集群,应该知道集群有两类操作,一种是集群资源类的操作,比如主题和用户信息的创建删除、限流配额信息的配置。一种是数据资源类的操作,比如生产消费某个数据。

集群资源类的操作属于运维类操作,一般需要运维部门人员来管理。数据资源类的操作一般是研发人员在使用。因为职能不同,比如研发一般是不允许执行集群的配置变更的。 所以这两类操作是需要隔离的

这两类资源的访问控制,在实现上有两种思路。

  • 独立两条链路,比如数据操作(生产和消费)使用TCP链路,集群资源的操作使用HTTP链路。
  • 同一条链路上实现两种操作,数据操作和集群资源操作在同一条TCP或HTTP链路上完成,然后通过接口或资源类型维度的鉴权来实现管控。

我们先看两条链路的方案。业务方主要访问数据链路,运维方主要访问资源链路。它的好处是资源类操作和数据类操作分开,从而在内核层面对鉴权控制实现的成本更低,编码量更少。在集群的交付和运营层面,无需额外配置访问权限,配置维护成本较低。Pulsar和RabbitMQ用的就是这种方案。

另一种单条链路的方案,在一条链路上完成所有的操作,无需在内核中开启两个Server。坏处就是需要额外设计多个维度(比如集群、主题、配置相关)的权限控制,内核的编码量较多,而且在集群运维时的配置成本也较高。业界的Kafka、RocketMQ用的就是这种方案。

两种不同选择的主要原因主要和当时研发人员的选择有关。从个人的角度,我比较建议第一种两条链路的方案。因为从内核实现和运维配置的角度来看,开发和配置的成本会低很多。

访问控制机制ACL

即使完成了数据和资源链路的独立,我们在数据链路内还是会包含生产、消费两个行为,另外可能有幂等、事务等特性,在资源链路里面也会有资源的增删改查等操作。在一些情况下,我们需要对这些操作做更细粒度的控制,这时候就需要访问控制技术(ACL)登场了。

如果你用过任何一款消息队列,肯定会听过ACL。ACL全称是访问控制机制,简单来说,就是解决“某个资源能不能被访问,能被谁访问”的问题。因此ACL包含两部分:一是定义好哪些行为和资源需要进行鉴权,二是如何实现鉴权。

从被访问主体的角度(即哪些行为和资源需要鉴权),一般分为三类。

  • 资源:主要对主题/ Queue、消费分组/订阅、集群三类资源做访问控制。另外一些消息队列独有的概念也会有需要做访问控制,比如RabbitMQ的Exchange。
  • 操作:主要分为读、写、创建、删除、修改、配置等,比如允许生产消费数据、允许创建删除修改Topic、允许修改集群配置。
  • 接口:一般会限制对集群接口的访问,比如限制某些用户不能访问某些接口。

从访问控制主体的角度(即如何实现鉴权),一般需要包含用户和IP两个维度。

  • 用户维度就是指控制某个用户的访问权限。
  • IP维度是指这个资源只能从某个或某些IP发起访问。

用户和IP的控制一般可以叠加使用,限制某些用户只能从某些IP发起访问。

上面两个方面,你基本可以在主流消息队列产品中对号入座,找到相应的实现。我们以Kafka、RabbitMQ具体看看主流消息队列的访问控制粒度。

Kafka在权限控制做得比较精细。从资源和API两个维度做了访问控制。资源主要分为主题、消费分组、集群三个维度,几乎涉及所有主要的API,控制操作包括读、写、创建、删除、修改、描述等等。

它做这么精细的一部分原因在于,它是在一条链路上实现数据和资源两类操作的,为了保证安全性,需要做更精细的管控。

我们再来看一下使用两条链路方案的RabbitMQ,RabbitMQ主要对Exchange、Queue、生产、订阅等维度做了控制,控制操作包括读、写、configure(资源的定义创建删除等)。

它没有做接口维度的控制,主要原因在于RabbitMQ的集群配置主要通过HTTP API完成。另外它的维度比Kafka少一些,因为RabbitMQ的架构更简单,所以接口和功能都更少。

那访问控制机制具体如何实现呢?

其实非常简单,在实现上就一个函数的工作量,流程分为两步。

  1. 请求接入的时候,获取到当前连接的用户信息或者IP信息。
  2. 在请求处理的开始,调用访问控制的实现函数(比如authorizeByResourceType),传入当前访问的操作(比如生产、消费、配置)以及用户或IP信息,和内存中的授权数据比较,返回是否具有权限。

虽然鉴权的实现逻辑比较简单,但在具体编码实现上, 消息队列一般都会支持一个可插拔的鉴权机制,即可以通过配置自定义的鉴权类来实现自定义的鉴权。

比如Kafka的鉴权配置就是可插拔的,通过配置 authorizer.class.name 的参数,来制定ACL的鉴权规则。它的实现就是定义接口,然后实现接口的检验函数来完成校验。

authorizer.class.name=kafka.security.authorizer.AclAuthorizer

超级用户

访问控制中还有一个非常必要的角色——超级用户。

在系统中有一个默认的超级用户,是非常必要的。如果没有超级用户,一旦分配出去的用户被不小心或者恶意修改,系统就无法恢复访问了,超级用户的存在可以很好地避免这个问题。另外,在系统运维过程中,超级用户会带来很多管理上的便利,比如运维负责人的临时、紧急状态的操作。

超级用户的配置一般是写死固定在配置文件当中的, 不能被修改和删除。在内核中会读取超级用户信息,然后在访问控制的时候,对是否是超级用户进行单独判断。Kafka就有这个机制,而RabbitMQ没有,导致RabbitMQ在日常运营过程中总会不时遇到用户密码被改动的情况,需要特殊处理恢复访问,运营很不方便也不安全。

总结

消息队列的安全性主要由四部分组成,分别是:网络间的隔离(网络隔离)、传输过程中的安全性(传输安全)、连接建立时的身份认证(集群认证)、连接建立后的访问控制(资源授权)。只要做好这四个方面,基本能够解决大部分的安全问题。

传输加密是可选的,它主要用在公网环境中。因为私有网络的安全性较高,一般不会发生数据窃取、修改等问题,所以在私有网络中使用的意义不是特别大。加密传输的缺点是会消耗较多资源,并且性能会有一定的下降。

业界的身份认证机制非常多样,它们在安全性上有细微的差别,比如安全性的强弱。但是基本所有的认证机制都可以满足需求,我们根据适合自己的需要来选就可以了。

访问控制ACL,从技术实现上来看并不是很复杂的,它的核心是定义好哪些行为和资源需要进行鉴权,从而保证系统各个环节的安全性。

思考题

从做一个Web后台管理系统的角度来看,传输加密、认证、鉴权分别对应系统中的什么呢?

欢迎分享你的想法,如果觉得有收获,也欢迎你把这节课分享给感兴趣的朋友。我们下节课再见!

上节课思考闭环

你还知道有哪些提高性能的编码技巧吗?比如在 Java 逻辑处理方面。

说一些比较常用的,不是特别全。

1. 锁、原子类的使用

2. 多线程和线程池的调优

3. 字符串处理优化

4. 合理地使用正则表达式

5. Map、HashMap、ConcurrentHashMap的选择

6. ArrayList和LinkList的使用