RabbitMQ安全设计从服务器声明队列(并从客户端使用)

时间:2018-01-13 22:44:22

标签: rabbitmq

我有一个测试应用程序(首先使用RabbitMQ),它运行在部分受信任的客户端上(因为我不希望他们自己创建队列),所以我将查看队列的安全权限和凭据客户联系。

对于消息传递,主要是从服务器到客户端的单向广播,有时是从服务器到特定客户端的查询(回复将通过replyTo队列发送,该队列专用于服务器侦听的客户端回复)。

我目前在服务器上有一个接收功能,用于寻找客户的“宣布”广播:

agentAnnounceListener.Received += (model, ea) =>
{
    var body = ea.Body;
    var props = ea.BasicProperties;
    var message = Encoding.UTF8.GetString(body);

    Console.WriteLine(
        "[{0}] from: {1}. body: {2}",
        DateTimeOffset.FromUnixTimeMilliseconds(ea.BasicProperties.Timestamp.UnixTime).Date,
        props.ReplyTo,
        message);

      // create return replyTo queue, snipped in next code section
};

我希望在上面的接收处理程序中创建返回主题:

var result = channel.QueueDeclare(
    queue: ea.BasicProperties.ReplyTo,
    durable: false,
    exclusive: false,
    autoDelete: false,
    arguments: null);

或者,我可以将收到的公告存储在数据库中,并通过此列表运行常规计时器,并在每次传递时为每个通道声明一个队列。

在这两种情况下,服务器将在未来使用这个新创建的频道向客户端发送查询。

我的问题是:

1)当从客户端接收消息时,在服务器上创建回复通道是否更好?或者如果我在外部(在计时器上)执行回复通道,是否存在声明已存在的队列的任何性能问题(可能有数千个终点)?

2)如果客户端开始错过行为,是否有任何方式可以启动它们(在接收功能中,我可以查找每分钟有多少消息并在满足某些条件时启动)?是否有任何其他过滤器可以在管道接收之前定义,以启动发送过多消息的客户端?

3)在上面的例子中,注意我的消息在每次运行中都会不断出现(相同的旧消息),我该如何清除它们?

2 个答案:

答案 0 :(得分:3)

我认为防止客户端创建队列只会使设计复杂化而没有太多的安全性好处。 您允许客户端创建消息。在RabbitMQ中,不容易阻止客户端使用消息充斥您的服务器。

如果您想对您的客户进行限价,RabbitMQ可能不是最佳选择。当服务器开始努力处理所有消息时,它会对速率进行限制automatically,但是您无法使用开箱即用的解决方案在服务器上基于每个客户端设置严格的速率限制。此外,通常允许客户端创建队列。

方法1 - 网络应用

也许您应该尝试使用Web应用程序:

  • 客户端使用您的服务器进行身份验证
  • 要宣布,客户端会向某个端点发送POST请求,即/api/announce,可能会提供一些允许他们这样做的凭据
  • 接收传入的消息,GET /api/messages
  • 确认已处理的讯息:POST /api/acknowledge

当客户确认收到时,您将从数据库中删除邮件。

通过这种设计,您可以编写自定义逻辑来限制或禁止行为不端的客户,并且您可以完全控制您的服务器

方法2 - RabbitMQ Management API

如果您仍想使用RabbitMQ,可以使用RabbitMQ Management API

来实现您的目标

您需要编写一个应用程序来定时查询RabbitMQ Management API并且:

获取所有当前连接,并检查每个连接的消息速率。

如果邮件速率超过您的阈值,请使用/api/permissions/vhost/user端点关闭连接或撤消用户的权限。

在我看来,如果您不需要所有排队功能(如工作队列或复杂的路由,您可以使用RabbitMQ开箱即用),Web应用程序可能会更容易。

答案 1 :(得分:1)

以下是针对您的方案的一些通用架构/可靠性建议。最后回答您的3个具体问题。

一般建筑理念

我不确定declare-response-queues-on-server方法是否会带来性能/稳定性好处;你必须对此进行基准测试。我认为实现您想要的最简单的拓扑结构如下:

  1. 每个客户端在连接时声明exclusive和/或autodelete匿名队列。如果客户'网络连接是如此粗略,以至于保持打开直接连接是不可取的,所以类似于Alex提出的" Web App"以上,并让客户端命中一个端点,代表他们声明一个独占/自动删除队列,并在客户没有经常联系时关闭连接(在消费者离开时自动删除队列)。只有当您无法从客户端调整RabbitMQ心跳以面对网络不可靠时,或者如果您可以证明您需要在Web应用层内部限制队列创建速率时,才应该这样做。
  2. 每个客户端的队列绑定到广播主题交换,服务器使用该交换来传送广播消息(通配路由密钥)或特定目标消息(仅匹配一个客户端队列名称的路由密钥)
  3. 当服务器需要从客户端获得回复时,您可以让服务器在发送"响应所需的"之前声明响应队列;消息,并对消息中的响应队列进行编码(基本上就是您现在正在做的事情),或者您可以在客户端构建语义,在这些客户端中,他们会在尝试{{{{{{{{{{{{{ 1}}(mutex)再次使用,将其响应发布到自己的队列,并确保服务器在关闭服务器消耗和恢复正常广播语义之前在规定的时间内消耗这些响应。但是,第二种方法要复杂得多,而且可能不值得。
  4. 防止客户压倒RabbitMQ

    可以减少服务器负载并帮助防止客户端使用RMQ操作执行服务器的事情包括:

    • 在所有队列上设置适当的低最大长度阈值,因此服务器存储的消息量永远不会超过客户端数量的某个倍数。
    • 设置每个队列的过期或每个消息的过期,以确保过时的消息不会累积。
    • 速率限制特定RabbitMQ操作非常棘手,但您可以在TCP级别进行速率限制(使用例如HAProxy或其他路由器/代理堆栈),以确保您的客户不会发送太多数据,或者一次打开太多连接。根据我的经验(只有一个数据点;如果有疑问,基准测试!)RabbitMQ对每次摄取的消息的计数的关注程度低于数据量最大可能的每个消息大小被摄取。很多小消息通常都可以;一些巨大的可能导致延迟峰值,否则,对TCP层的字节进行速率限制可能会允许您在必须重新评估之前将这个系统扩展到很远。

    具体答案

    鉴于上述情况,我对您具体问题的回答是:

    问:您是否应该在服务器上创建回复队列以响应收到的消息?

    答:是的,可能是。如果您担心队列创建率 因此,您可以对每个服务器实例进行速率限制。看起来您正在使用Node,因此您应该能够使用该平台的现有solutions之一为每个节点服务器实例创建一个队列创建速率限制器,除非您有很多成千上万的服务器(不是客户端)应该允许您在重新评估之前达到非常大的规模。

    问:根据客户端操作声明队列会对性能产生影响吗?或者重新声明队列?

    A:基准并看到!重新声明可能没问题;如果你正确率限制你可能根本不需要担心这个问题。根据我的经验,大量的队列声明事件会导致延迟有点上升,但不要破坏服务器。但那只是我的经历!每个人的场景/部署都不同,因此无法替代基准测试。在这种情况下,您可以通过稳定的消息流启动发布者/消费者,例如跟踪消息。发布/确认延迟或消息接收延迟,rabbitmq服务器负载/资源使用情况等。虽然一些发布/使用对正在运行,但是以高并行方式声明大量队列并查看指标会发生什么。同样根据我的经验,如果任何明显的负载峰值,队列的重新声明(幂等)也不会造成太大的影响。更重要的是要建立新的连接/渠道的速度。您还可以在每个服务器的基础上非常有效地对队列创建进行速率限制(请参阅我对第一个问题的回答),所以我认为如果您正确实现了这一点,您很长时间都不需要担心这一点。 RabbitMQ的性能是否会受到存在的队列数量(与声明率相反)的影响,这将是另一个基准测试。

    问:你可以根据不良行为踢客户吗?留言率?

    答:是的,虽然设置有点棘手,但这可以通过至少某种优雅的方式完成。您有两种选择:

    选项一:您提出的建议:跟踪您服务器上的消息率,正如您正在做的那样,以及"踢"基于此的客户。如果您有多个服务器,并且需要编写存在于消息接收循环中的代码,并且在RabbitMQ实际将消息传递给您的服务器的消费者之前不会出现问题,则会出现协调问题。这些都是显着的缺点。

    选项二:使用max-length和死信交换构建一个"踢坏客户"剂。 RabbitMQ队列上的length limits告诉队列系统"如果队列中有多于X的消息,则删除它们或将它们发送到死信交换(如果已配置)"。 Dead-letter exchanges允许您将大于长度(或满足其他条件)的消息发送到特定队列/交换。以下是如何将这些组合用于检测发布消息的客户端过快(比服务器可以消耗它们的速度快)并踢客户端:

    1. 每个客户端声明它的主exclusive队列的最大长度为某个数字,比如说$clientID_to_server,除非客户端是"超越"否则永远不会在队列中累积X #34;服务器。该队列有一个死信主题交换ratelimit或一些常量名称。
    2. 每个客户端声明/拥有一个名为$clientID_overwhelm的队列,其最大长度为1.该队列绑定到ratelimit交换,路由密钥为$clientID_to_server。这意味着当邮件以非常快的速度发布到$clientID_to_server队列以使服务器跟上时,邮件将被路由到$clientID_overwhelm,但只有一个会被保留(所以你不要#39;填写RabbitMQ,并且每个客户端只存储X+1条消息。
    3. 启动一个简单的代理/服务,它发现(例如通过RabbitMQ Management API)所有连接的客户端ID,并从所有*_overwhelm队列中消耗(仅使用一个连接)。每当它在该连接上收到消息时,它从该消息的路由键获取客户端ID,然后踢该客户端(通过在您的应用程序中执行带外操作;删除该客户端' s {{ 1}}和$clientID_to_server队列,因此在下次客户端尝试执行任何操作时强制出错;或者通过RabbitMQ管理API中的$clientID_overwhelm端点关闭该客户端与RabbitMQ的连接 - - 这是非常侵入性的,只有在你真的需要的时候才应该这样做。这项服务应该很容易编写,因为除了RabbitMQ之外,它不需要与系统的任何其他部分协调状态。但是,使用此解决方案,您将从行为不端的客户端丢失一些消息:如果您需要保留所有这些消息,请删除压倒性队列的最大长度限制(并冒充满RabbitMQ的风险)。
    4. 使用这种方法,您可以根据RabbitMQ 检测发送垃圾邮件的客户端,而不仅仅是根据您的服务器发生的。您可以通过向客户端发送的消息添加per-message TTL来扩展它,并在消息在队列中停留超过一定时间时触发死信踢行为 - 这将改变伪来自"当服务器消费者被消息 count "当服务器消费者落后于消息交付时间戳"。

      问:为什么每次运行都会重新传递消息,我该如何摆脱它们?

      答:使用确认或noack(但可能是确认)。在"收到"只是将它拉入您的消费者,但不会从队列中弹出它。它就像一个数据库事务:要最终弹出它,你必须在收到后才能确认它。最后,你可以在" noack"模式,这将导致接收行为以您认为的方式工作。然而,被警告,noack模式带来了很大的权衡:因为RabbitMQ正在向您的消费者带外传递消息(基本上:即使您的服务器被锁定或睡眠,如果它已经发布了/connections,兔子正在向它推送消息),如果你在noack模式下消费,那么当它们将它们推送到服务器时,这些消息会被永久地从RabbitMQ中删除,所以如果服务器崩溃或关闭之后再消耗掉它的消息。本地队列"任何消息等待接收,这些消息将永远丢失。如果您不丢失信息非常重要,请注意这一点。