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服务器免受这些查询,但是它们都不是万无一失的。重要的是,我们要知道有哪些方法可用来限制,并了解他们的优缺点,然后采取最优的决定!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。