英文原版地址:https://www.howtographql.com/...

GraphQL为客户端提供强大的能力。但是拥有强大的能力时,也会带来更大的风险。

由于客户端有可能使用非常复杂的查询,因此我们的服务器必须能够妥善处理。这些查询可能是来自恶意客户端的滥用查询,或者可能只是合法客户端使用的非常大的查询。在这两种情况下,客户端可能会将您的GraphQL服务器崩溃。

我们将在本章中介绍一些减轻这些风险的策略。我们将以最简单到最复杂的顺序来说明,并看看这些方式的利弊。

超时策略

第一个策略,也是最简单的策略是使用简单的超时来防范大型查询。这不需要服务器了解有关传入查询的任何内容。服务器需要知道的仅仅是允许查询的最长时间。

例如,配置了5秒超时的服务器将停止执行超过5秒钟执行的任何查询。

超时的优势

  • 操作简单

  • 大多数策略都会使用超时作为最终保护

超时的缺点

  • 即使有超时策略,也可能会造成不好的后果

  • 有时难以实施。在一段时间之后切断连接可能会导致奇怪的行为。

最大查询深度

正如我们之前所述,使用GraphQL的客户可以随意写出任意的复杂查询。由于GraphQL模式通常是嵌套的,这意味着客户端可以写出如下所示的查询:

query IAmEvil {
  author(id: "abc") {
    posts {
      author {
        posts {
          author {
            posts {
              author {
                # that could go on as deep as the client wants!
              }
            }
          }
        }
      }
    }
  }
}

如果我们可以阻止客户滥用这样的查询深度呢? 在了解定义的模式时,可以让你了解合法查询的深度。这实际上是可以实现的,并且通常称为最大查询深度。

通过分析查询文档的AST,GraphQL服务器能够根据其深度拒绝或接受请求。

例如,配置了最大查询深度为3的服务器,以及以下查询文档。红色方框选中的所有内容都被认为深度太深,查询无效。

图片描述

使用最大查询深度设置的graphql-ruby服务,我们得到以下返回结果:

{
  "errors": [
    {
      "message": "Query has depth of 6, which exceeds max depth of 3"
    }
  ]
}

最大查询深度优点

  • 由于静态分析了文档的AST,因此查询甚至不执行,所以不会在GraphQL服务器上增加负担。

最大查询深度缺点

  • 只有深度往往不足以涵盖所有滥用查询。 例如,在根节点上请求大量的查询将是代价巨大的,但不太可能被查询深度分析器阻止。

查询复杂性

有时,查询的深度还不足以真正了解GraphQL查询的开销。在很多情况下,我们的模式中的某些字段比其他字段更复杂。

查询复杂性允许您定义这些字段的复杂程度,并限制最大复杂度的查询。这个想法是通过使用一个简单的数字来定义每个字段的复杂程度。一个常见的默认设置是给每个字段一个复杂的1。以这个查询为例:

query {
  author(id: "abc") { # complexity: 1
    posts {           # complexity: 1
      title           # complexity: 1
    }
  }
}

一个简单的加法,告诉我们查询的复杂性是3。如果我们在我们的架构上设置最大复杂度为2,则此查询将会失败。

如果posts字段实际上比作者字段复杂度高很多呢?我们可以为该领域设置不同的复杂性。我们甚至可以根据参数设置不同的复杂性! 我们来看看一个类似的查询,其中posts会根据传入的参数去确定复杂性:

query {
  author(id: "abc") {    # complexity: 1
    posts(first: 5) {    # complexity: 5
      title              # complexity: 1
    }
  }
}

查询复杂性的优点

  • 可以覆盖比更多的用例。

  • 通过静态分析复杂性,在执行前拒绝查询。

查询复杂性缺点

  • 很难实现完美

  • 如果需要开发时预估复杂性,我们如何保持状态最新?我们一开始怎么能知道查询成本?

  • Mutations 很难估计。如果他们有一个难以衡量的附加操作,如在后台排队执行的任务怎么办?

节流

到目前为止,我们看到的解决方案都是会阻止滥用服务器的查询。像这样使用它们的问题是,它们会阻止大量查询,但不会阻止客户端生成出大量查询!

在大多数API中,使用简单的节流方式是,阻止客户端频繁地请求资源。GraphQL有点特别,因为调节请求数并没有真正帮助我们。即使是很少的请求也可能是大量的查询。

事实上,我们不知道客户端定义了多少请求是可以接受的。那么我们如何来限制客户端呢?

基于服务器执行时间的调节

我们可以通过查询执行时的服务器耗时,来估计查询的复杂程度。我们可以使用这种式来限制查询。凭借对系统的了解,您可以提出客户端可以在特定时间范围内使用的最大服务器时间。

我们还决定随着时间的推移,客户端添加多少服务器时间。这是一个经典的leaky bucket 算法。请注意,还有其他节流算法,但这些算法超出了本章的范围。在下面的例子中我们将使用leaky bucket。

让我们想象一下,我们将允许的最大服务器时间(Bucket Size)设置为1000ms,客户端每秒获得100ms的服务器时间(Leak Rate),mutation 如下:

mutation {
  createPost(input: { title: "GraphQL Security" }) {
    post {
      title
    }
  }
}

这个mutation平均需要200ms才能完成。实际上,时间可能会有所不同,但我们假设为了这个例子,它总是需要200ms才能完成。

这意味着在1秒内调用此操作超过5次的客户端将被阻止,直到更多的可用服务器时间添加到客户端。

经过两秒钟(100ms加秒),我们的客户可以一次调用createPost。

正如你所看到的,基于时间的调节是限制GraphQL查询不错的方式,因为复杂的查询将最终消耗更多的时间,这意味着你不能频繁地调用它们,而较小的查询可能被更频繁地调用,因为它们将非常快速地计算。

但如果GraphQL API是公开的,向客户端提出这些限制条件就不那么容易了。在这种情况下,服务器耗时并不能很好地告知客户端,客户端也不能准确的估计他们的查询所需要的时间,在不先试着请求的情况下。

还记得我们之前提到的最大复杂度?如果我们根据这个调节,会怎么样?

基于查询复杂度的调节

基于查询复杂度的调节是与客户端合作的好方法,客户端可以遵循schema中的限制。

我们使用与“查询复杂性”部分中使用的相同的复杂性示例:

query {
  author(id: "abc") {    # complexity: 1
    posts {              # complexity: 1
      title              # complexity: 1
    }
  }
}

我们知道这个查询的成本是基于复杂度的3。就像时间流逝一样,我们可以得知客户可以使用的每次最高成本(Bucket Size)。

如果最大成本为9,我们的客户只能三次运行此查询,不允许查询更多。

这些原理与我们的时间节制相同,但现在将这些限制传达给客户端后。客户甚至可以自己计算查询成本,而无需估计服务器时间!

GitHub公共API实际上使用这种方法来扼制客户端。看看他们如何对用户表达这些限制:https://developer.github.com/v4/guides/resource-limitations/。

总结

GraphQL非常适合用于客户端,因为给予了更多的功能。但是,强大的功能也带来了风险,担心客户端会以非常昂贵的查询来滥用GraphQL服务器。

有许多方法来保护您的GraphQL服务器免受这些查询,但是它们都不是万无一失的。重要的是,我们要知道有哪些方法可用来限制,并了解他们的优缺点,然后采取最优的决定!


恒1910
477 声望35 粉丝

战五渣,刷怪打钱阶段,等着上天


引用和评论

0 条评论