跳至主要内容

生产注意事项

概述

Grafast 最适合使用 静态查询,这很可能是您已经在使用的(除非您使用字符串连接来构建查询,在这种情况下,您应该切换到使用 变量)。

假设您通过 HTTP API 使用 GraphQL 模式,我们建议您使用 持久化操作(例如通过 @grafserv/persisted)作为操作“允许列表”。这有助于保护您的服务器免受恶意行为者发送恶意查询以试图引发 拒绝服务 攻击。

如果您选择不使用持久化操作,或者如果您想更加安全(尤其是在您的团队对添加分页限制或谨慎放置变量方面没有严格的纪律时),那么您还应该考虑设置计划和执行超时

const preset = {
grafast: {
timeouts: {
/** Planning timeout in ms */
planning: 500,

/** Execution timeout in ms */
execution: 30_000,
},
},
};

无论哪种情况,跟踪恶意行为者并阻止/限制来自他们的请求可能都是明智之举。你通常可以通过 Web 服务器中的中间件来实现这一点。

下面,我们将更详细地介绍这些主题。

静态查询

通过字符串拼接构建 GraphQL 查询通常被认为是不好的做法(在 Grafast 和更广泛的 GraphQL 生态系统中都是如此)。

function getUserDetails(userId) {
// DON'T DO THIS
const source = `
query UserDetails {
userById(id: ${userId}) { # <<< STRING CONCATENATION IS BAD!
username
avatarUrl
}
}
`;
return runGraphQLQuery(source);
}

相反,请一次性声明查询文本(“静态查询”),然后使用 GraphQL 变量 在查询文本旁边传递参数。

// Declare the query once:
const UserDetailsQuery = /* GraphQL */ `
query UserDetails($userId: Int!) {
userById(id: $userId) {
username
avatarUrl
}
}
`;

function getUserDetails(userId) {
// Run the static query using the dynamic variable:
return runGraphQLQuery(UserDetailsQuery, { userId });
}

通过 HTTP,这可能看起来像这样

POST /graphql HTTP/1.1
Host: example.com
Content-Type: application/json
Accept: application/json

{
"query": "query UserDetails($userId: Int!) { userById(id: $userId) { username avatarUrl } }",
"variables": {
"userId": 7
}
}

在 Grafast 中,这尤其重要,因为每次我们看到一个新的 GraphQL 文档时,都需要对其进行规划,因此通过反复使用同一个文档,我们可以将规划成本降低很多倍。

持久化操作

如果你不打算允许第三方对你的 API 运行任意操作,那么使用 持久化操作 作为查询白名单是保护任何 GraphQL 端点(Grafast 或其他)的强烈推荐解决方案。这种技术确保只有你在自己的应用程序(网站、移动应用程序、桌面应用程序等)中使用的操作才能在服务器上执行,从而防止恶意(或仅仅好奇)的行为者执行比你编写的操作更昂贵的操作。

这种技术适用于绝大多数用例,并支持许多 GraphQL 客户端,但它确实有一些注意事项。

  • 你的 API 只能接受你批准的操作,因此如果你希望第三方运行任意自定义操作,它就不适合。
  • 你必须能够在应用程序/网页的构建时从每个操作生成一个唯一的 ID(例如哈希值) - 你必须使用 静态查询。重要的是要注意,这仅适用于操作文档本身,变量当然可以在运行时更改。
  • 您必须有一种方法可以将这些静态操作从应用程序构建过程共享到服务器,以便服务器知道 ID 代表什么操作。
  • 您应该注意不要在操作中的危险位置使用变量;例如,如果您要使用allPosts(first: $myVar),恶意攻击者可以将$myVar设置为2147483647,从而导致您的服务器处理尽可能多的数据。尽可能使用固定限制、条件和顺序,即使这意味着需要额外的静态操作(或者,让您的模式强制执行这些内容的存在和/或有效范围)。
  • 持久化操作并不能保护您免受自己编写昂贵查询的影响;明智的做法是将此技术与成本估算技术结合使用,以帮助指导您的开发人员并避免意外编写昂贵的查询。

Grafserv 通过开源 @grafserv/persisted 模块对持久化操作提供了一级支持;我们建议绝大多数用户使用它。如果您使用的是 Envelop 支持的服务器,请查看 @envelop/persisted-operations

拒绝服务

TL;DR:使用 持久化操作,或配置 超时

Grafast,与所有技术一样,都做出了权衡。Grafast 的主要权衡是,它在第一次看到 GraphQL 操作时(“规划”)就会工作,以便显着减少每次运行该操作所需的工作量(即使使用不同的变量/上下文等)。有时这种规划在同一个请求中就得到了回报,但通常可能需要一两个额外的请求才能通过效率提升来弥补规划成本(此后就是纯收益!)。

由于规划是同步 JavaScript 代码,而 Node.js 是单线程的,因此这种规划会在它完成时短暂地阻塞事件循环。对于简单的操作,这可能只需要几毫秒,但对于更大更复杂的请求,它可能会增长,尤其是如果您使用的是具有复杂deduplicateoptimizefinalize 方法的步骤类。对于大型查询的规划,尤其是涉及多态性的查询,规划时间超过 50 毫秒并不罕见。

攻击者可能会试图利用这种规划时间来发动拒绝服务攻击,因此我们必须确保不允许攻击者让我们的服务器规划过于复杂的查询。为此,主要有两种方法

  1. 使用“允许列表”来批准查询 - 请参阅 持久化操作
  2. 限制请求 - 请参阅 限制

您可以使用其中一项或最好同时使用这两项技术来保护您的服务器。

限制

有很多方法可以限制服务器接受的请求。通常,您希望在不影响合法用户的情况下抓住恶意行为者,因此您正在寻找异常 - 超出正常范围的使用 - 并且您希望对这些异常进行限速或阻止。

除此之外,您还需要确保任何单个请求都不会占用超过特定阈值的时/资源,这就是超时发挥作用的地方。

限速/阻止

您的 Web 服务器通常最适合决定是否执行请求。

一个简单的保护措施是要求使用您的 API 进行身份验证。这并不适合所有 API,但如果对您有效,它可以显着减少您的攻击面 - 特别是保护您免受无目标的自动化攻击。另一种防止无目标攻击的方法是检查请求的来源,或者要求包含随机生成的值,例如 CSRF 令牌。

每个版本的应用程序(网站、移动应用程序等)可能都有一小部分静态查询(可能几百个)。GraphiQL 或类似 IDE 的用户不太可能在任何五分钟内向您的服务器发送超过 50 个新的唯一查询。一种选择是配置您的服务器,以便在特定时间段内计算来自特定来源的唯一查询数量,并在达到限制后阻止来自该来源的未来查询。

类似地,您可能会跟踪每个请求执行所需的时间,并为每个客户端提供每个时间窗口的最大执行时间 - 一旦超过了此时间,您就可以阻止来自该客户端的未来请求,直到他们的窗口刷新。

大多数都是 Web 服务器的标准配置,您应该能够在您的服务器生态系统中找到模块来帮助您解决它们。

超时

一旦您开始执行 GraphQL 操作,您可能不应该让它永远运行。Grafast 为您提供了两种配置超时的选项:一个 planning 超时,它在计划操作时应用,以及一个 execution 超时,它在每次执行计划时应用。超时在 preset 中配置,它是您传递给 grafast()execute() 的第二个(可选)参数(或者是在 grafserv 中使用的配置文件)。

const preset = {
grafast: {
timeouts: {
/** Planning timeout in ms */
planning: 500,

/** Execution timeout in ms */
execution: 30_000,
},
},
};

计划超时

计划超时在每次计划操作时应用。

计划可能很耗时,尤其是在服务器刚启动且 V8 还没有机会预热 JIT 缓存时。因此,我们增加了对前几个计划操作允许的超时时间。

计划超时时间仅在计划查询的某些阶段进行检查,因此可能会超过(通常只超过几十毫秒)。如果您发现这有问题,请联系我们,我们可以讨论在更多位置添加超时检查。

最重要的是,请注意,操作所需的计划时间会根据机器的负载、机器的性能以及之前计划的内容而有所不同。我们建议设置一个较高的限制,例如 500 毫秒,并将其与上面部分中描述的速率限制相结合。

执行超时

每次执行操作计划时都会应用执行超时时间。

当第一次看到操作时,它将进行计划(遵守计划超时时间),然后执行(遵守执行超时时间) - 执行超时时间包括计划。

执行超时时间仅在异步步骤(正常步骤 - 那些没有step.isSyncAndSafe === true的步骤)即将执行之前进行检查 - 假设同步步骤足够快,无需对它们应用超时时间。请注意,这意味着步骤本身负责遵守超时时间 - 它们将在“extra”参数传递给execute()时传递一个stopTime属性,当performance.now()的值大于或等于stopTime时,它们应该中止其活动操作。