接上篇 —— 如何使用 GraphQL-进阶教程:工具和生态 —— 继续翻译How to GraphQL 系列教程。
了解 GraphQL 的不同方面的安全性和策略,例如超时、最大查询深度、查询复杂度和节流。
基础和进阶系列翻译已完成:
GraphQL 为客户端提供了强大的能力。但是强大的能力伴随着巨大的责任 🕷。
由于客户端可以进行非常复杂的查询,因此服务端必须准备好正确处理它们。这些查询可能是来自恶意客户端的故意滥用查询,也可能只是合法客户端使用的非常大的查询。在这两种情况下,客户端都可能破坏 GraphQL 服务端。
有一些策略可以减轻这些风险。将在本章中按从最简单到最复杂的顺序进行介绍,并探讨它们的优缺点。
超时
第一个也是最简单的策略是使用超时来防御非常大的查询。此策略是最简单的,因为它不需要服务端了解有关传入查询的任何信息。服务端只知道查询允许的最长时间。
例如,配置为 5 秒钟超时的服务端将停止执行耗时超过 5 秒钟的任何查询。
超时优点
- 易于实现。
- 大多数策略仍将使用超时作为最终保护。
超时缺点
- 即使不超时也可能造成损坏。
- 有时难以实现。一定时间后断开连接可能会导致奇怪的行为。
最大查询深度
如前所述,使用 GraphQL 的客户端可以构建他们想要的任何复杂查询。由于 GraphQL schema 通常是循环图,这意味着客户端可以编写如下查询:
1 | query IAmEvil { |
怎么样可以防止客户端这样滥用查询深度?分析 schema 可以使你了解合法查询的深度。这实际上是可行的,通常称之为最大查询深度。
通过分析查询文档的抽象语法树(AST),GraphQL 服务端能够根据其深度拒绝或接受请求。
例如,配置了最大查询深度为 3
的服务端和以下查询文档。红色标记内的所有内容都被认为太深,查询无效。
使用带有最大查询深度设置的 graphql-ruby
,可以得到以下结果:
1 | { |
最大查询深度优点
- 由于文档的 AST 是静态分析的,因此查询甚至不会执行,这不会给 GraphQL 服务端增加负担。
最大查询深度缺点
- 仅有深度通常不足以覆盖所有滥用查询。例如,在根节点上请求大量节点的查询将有很大耗费,但不太可能被查询深度分析器阻止。
查询复杂度
有时,查询的深度不足以真正了解 GraphQL 查询的大小或耗费。在很多情况下,已知 schema 中的某些字段要比其他字段计算更复杂。
查询复杂度允许定义这些字段的复杂度,并以最大复杂度限制查询。这个想法是通过使用一个简单的数字来定义每个字段的复杂程度。常见的默认设置是使每个字段的复杂度均为 1
。以下述查询为例:
1 | query { |
一个简单的加法运算的查询复杂度总计为 3。如果我们在 schema 上将最大复杂度设置为 2,则此查询将失败。
比如 posts
字段实际上比 author
字段复杂得多,此时怎么办?可以为字段设置不同的复杂度。甚至可以根据参数设置不同的复杂度!看一个类似的查询,其中 posts
具有可变的复杂度,具体取决于其参数:
1 | query { |
查询复杂度
- 比简单查询深度覆盖更多的用法。
- 通过静态分析复杂度,使在执行查询之前就能拒绝查询。
查询复杂度缺点
- 难以完美实现。
- 如果开发者估算复杂度,如何保持实时更新?首先如何计算出耗费?
- 对变更来说很难估计。如果有难以衡量的副作用,例如后台排队执行时该怎么办?
节流
到目前为止,我们所看到的解决方案对于阻止滥用查询导致服务端崩溃非常有效。像这样单独使用它们的问题在于,将停止大规模查询,但不会停止正在进行大量中等规模查询的客户端!
在大多数 API 中,可以使用简单的限制来阻止客户端过于频繁地请求资源。GraphQL 有点特殊,因为限制请求数量并没有真正有效。如果规模很大,那么即使是几个查询也可能过量。
实际上不知道可以接受多少请求,因为它们是由客户端定义的。那么可以用什么办法给客户端节流呢?
基于服务端时间的节流
估计查询耗费的一个好办法是计算服务端的花费时间。可以使用这种启发式方法来限制查询。充分了解系统后,可以得出客户端可以在特定时间范围内耗费服务端的最大时间。
我们还决定了随着时间的推移,有多少服务端时间被添加到客户端上。这是一种经典的漏桶算法。注意,还有其他节流算法,但是不在本章的讨论范围之内。在下面的示例中将使用漏桶算法节流。
假设允许的最大服务端时间(Bucket Size)设置为 1000ms
,客户端每秒获得“服务端时间”(Leak Rate)为100ms
,且变更如下:
1 | mutation { |
平均需要 200ms
才能完成。实际上时间可能会有所不同,但是出于本例的考虑,假设它总是需要 200ms
才能完成。
这意味着客户端将在 1 秒内多次调用此操作 5 次以上,直到将更多可用服务端时间添加到客户端为止。
两秒钟后(每秒增加 100ms
),客户端可以调用一次 createPost
。
如你所见,基于时间的限制是限制 GraphQL 查询的一种好方法,因为复杂的查询最终将耗费更多的时间,这意味着可以减少调用它们的次数,而较小的查询的计算速度非常快,则可以被调用的次数更多。
如果 GraphQL API 是公开的,最好将这些限制条件传达给客户端。在这种情况下,服务端时间并不总是很容易传达给客户端,并且客户如果不先尝试就无法真正估计他们的查询将耗费多少时间。
还记得之前提到的最大复杂度吗?如果以此为基础进行节流应该怎么做?
基于查询复杂度的节流
基于查询复杂度的节流是一种很好的与客户端合作的办法,能帮他们遵守 schema 限制。
使用在“查询复杂度”章节中使用的相同复杂度示例:
1 | query { |
我们知道此查询基于复杂度的成本为 3
。就像时间节流一样,可以得出客户端每次使用时的最大耗费(Bucket Size)。
在最大耗费为 9
的情况下,客户端只能运行 3
次此查询,然后泄漏率会禁止进行更多查询。
这些原理与时间节流相同,但是将这些限制传达给客户端会更好。客户端甚至可以自己计算查询耗费,无需估算服务端时间!
GitHub 公共 API 实际上使用这种方法来限制其客户端。看一看他们如何向用户传达这些限制:https://developer.github.com/v4/guides/resource-limitations/。
总结
GraphQL 非常适合客户端使用,因为它为客户端提供了更多能力。但是,这种强大能力也使他们可以滥用使 GraphQL 服务端的查询耗费过大。
有很多方法可以保护 GraphQL 服务端免受这些查询的侵害,但是没有一种是完全百分百有效的。重要的是要知道可用的选择及其优缺点,以便做出最佳决策!
前端记事本,不定期更新,欢迎关注!