持久查询连接
在最小化请求延迟的同时保护您的图。
要解决的问题
与使用固定URL加载数据的REST API不同,GraphQL提供了一个丰富的查询语言,可以用来描述应用程序数据需求的结构。这是一项技术上的伟大进步,但是这也带来了一定的代价:GraphQL查询字符串通常比REST URL长得多——在某些情况下,可能长达数千字节。
在实践中,我们已经看到GraphQL查询的大小超过10 KB 仅查询文本。与50-100字符的简单URL相比,这是一个显著的额外开销。当考虑到客户端的上传速度通常是整个链中带宽最受限的部分时,大型查询可能会成为客户端性能的瓶颈。
恶意行为者可以通过发送大量复杂请求来利用GraphQLAPI,从而压倒服务器和中断服务。这些攻击者可以通过滥用GraphQL的查询灵活性来创建深嵌套、资源密集的查询,这些查询会导致数据获取量过大。
解决方案
Apollo支持两个相互关联但独立的特性,称为自动持久化查询(APQs)和持久化查询。使用这两个特性,客户端可以通过发送操作ID而不是整个操作字符串来执行GraphQL操作。一个操作ID是对完整操作字符串的哈希值。通过ID进行查询可以显著降低大操作字符串的延迟和带宽使用。
持久化查询与APQ的区别
持久化查询功能要求将操作注册到一个持久化查询列表(PQL)中,以便PQL可以作为您的一方应用程序制作的操作白名单。因此,持久化查询既是性能功能,也是安全功能。
使用APQs时,如果服务器找不到客户端提供的操作ID,服务器将返回一个错误,指出需要完整的操作字符串。如果Apollo客户端收到这个错误,它将自动使用完整的操作字符串重试操作。
如果您只想提高请求延迟和带宽使用,APQ满足您的使用案例。如果您还想要通过操作白名单来保护您的超级图,应将操作注册到PQL中。
有关持久化查询和APQ之间区别的更多详细信息,请参阅GraphOS持久化查询文档。
实现步骤
由于持久化查询需要您预先注册操作,因此具有额外的实现步骤。
我们建议您在实现时遵循以下顺序:
实现步骤 | 是否需要PQs? | 是否需要APQs? |
---|---|---|
1. 生成操作清单 | ✅ | -- |
2. 将操作清单发布到一个PQL | ✅ | -- |
3. 客户端在发出请求时启用持久化查询 | ✅ | ✅ |
本文的其余部分详细介绍了这些步骤。
持久化查询还需要您创建并连接到一个PQL,以及配置您的路由器以接收持久化查询请求。本文档仅描述客户端为创建操作清单并发送持久化查询请求需要采取的步骤。有关持久化查询其他配置方面的更多信息,请参阅GraphOS持久化查询文档。
0. 要求
使用持久查询进行安全列表需要有以下要求:
- Apollo客户端Web(v3.7.0+)
- 需要
@apollo/generate-persisted-query-manifest
- 需要
@apollo/persisted-query-lists
- GraphOS网关(v1.25.0+)
- GraphOS企业计划
您可以使用APQ与以下版本的Apollo客户端Web、Apollo服务器以及Apollo网关核心进行结合使用:
- Apollo客户端Web(v3.2.0+)
- Apollo服务器(v1.0.0+)
- Apollo网关核心(v0.1.0+)
注意:您可以使用任一个Apollo服务器或Apollo网关核心来结合使用APQ,它们不需要同时使用。
1. 生成操作清单
此步骤仅适用于持久查询,而不适用于APQ。
操作清单是一本清单,作为安全列表,使得GraphOS网关可以使用它来检查传入的请求。您可以使用以下@apollo/generate-persisted-query-manifest
工具包来生成清单:
- 将以下
@apollo/generate-persisted-query-manifest
包作为开发依赖项安装:
npm install --save-dev @apollo/generate-persisted-query-manifest
- 然后使用其CLI从中提取你的应用的查询
npx generate-persisted-query-manifest
生成的 操作 清单看起来像这样:
{"format": "apollo-persisted-query-manifest","version": 1,"operations": [{"id": "e0321f6b438bb42c022f633d38c19549dea9a2d55c908f64c5c6cb8403442fef","body": "query GetItem { thing { __typename } }","name": "GetItem","type": "query"}]}
你可以选择在你的项目根目录创建一个配置文件来覆盖默认选项。请参阅包的 README 以获取详细信息。
为了自动更新每个新应用的清单,请将 generate-persisted-query-manifest
命令包含到您的 CI/CD 管道中。
2. 将清单发布到 PQL
💡 小贴士
确保您的 Rover CLI 版本为 0.17.2
或更高版本。Rover 的较早版本不支持将操作发布到 PQL。下载最新版本。
在您 生成了一个操作清单后,您可以使用 Rover CLI 按如下方式将其发布到您的 PQL:
rover persisted-queries publish my-graph@my-variant \--manifest ./persisted-query-manifest.json
- 该
my-graph@my-variant
参数是 PQL 链接到的任何变体的 graph ref。- Graph refs 的格式为
graph-id@variant-name
。
- Graph refs 的格式为
- 使用
--manifest
选项提供您要发布的清单路径。
ⓘ 注意
该 persisted-queries publish
命令假定清单使用 Apollo 客户端工具生成的 格式生成。该命令还支持由 Relay 编译器生成的清单,通过添加 生成的清单 通过添加 --manifest-format relay
参数。您的 Rover CLI 版本必须为 0.19.0 或更高版本才能使用此参数。
该 persisted-queries publish
命令执行以下操作:
将所提供的清单文件中的所有操作发布到指定变体或指定 PQL 链接的 PQL 中。
- 将清单发布到 PQL 是累加的。PQL 中保留现有条目。
- 如果您发布了一个与现有 PQL 中的条目有相同
id
但细节不同的操作,则整个发布命令将由于错误而失败。
更新PQL应用到的任何其他变体,以便与这些变体相关的路由器可以获取它们更新的PQL。
与生成清单一样,最好在CI/CD管道中执行此命令,将新operations作为应用程序发布过程的一部分发布。您提供给Rover的API密钥必须具有角色为Graph Admin或Persisted Query Publisher。 Persisted Query Publisher是一个特殊角色,用于与rover persisted-queries publish
命令一起使用;具有此角色的API密钥没有其他对GraphOS中图的数据的访问权限,并且适合与应该被允许将operations发布到您图PQL的受信任第三方客户端开发者共享。
测试操作
您可以将一些测试operations发送以测试您是否成功发布了您的清单:
首先,启动您的GraphOS连接路由器:
APOLLO_KEY="..." APOLLO_GRAPH_REF="..." ./router --config ./router.yaml2023-05-11T15:32:30.684460Z INFO Apollo Router v1.18.1 // (c) Apollo Graph, Inc. // Licensed as ELv2 (https://go.apollo.dev/elv2)2023-05-11T15:32:30.684480Z INFO Anonymous usage data is gathered to inform Apollo product development. See https://go.apollo.dev/o/privacy for details.2023-05-11T15:32:31.507085Z INFO Health check endpoint exposed at http://127.0.0.1:8088/health2023-05-11T15:32:31.507823Z INFO GraphQL endpoint exposed at http://127.0.0.1:4000/ 🚀
然后,使用curl
发送POST请求,如下所示:
curl https://127.0.0.1:4000 -X POST --json \'{"extensions":{"persistedQuery":{"version":1,"sha256Hash":"dc67510fb4289672bea757e862d6b00e83db5d3cbbcfb15260601b6f29bb2b8f"}}}'
如果您的路由器PQL中包括一个ID与提供的sha256Hash
属性值匹配的操作,它将执行相应的操作并返回其结果。
3. 在ApolloClient
上启用持久查询
您使用持久查询 Apollo Link将操作发送为ID而不是完整的操作字符串。实现细节取决于您是否使用持久查询或APQs。
持久查询实现
持久查询链接包含在@apollo/client
包中:
npm install @apollo/client
持久查询实现还需要@apollo/persisted-query-lists
包。
安装@apollo/persisted-query-lists
包:
npm install @apollo/persisted-query-lists
该包的一个实用工具,generatePersistedQueryIdsFromManifest
,读取您操作清单中的操作ID,以便客户端可以使用它们进行请求。要这样做,将loadManifest
选项传递为一个返回您的清单的函数。我们建议使用动态导入以避免将清单配置与您的生产构建捆绑在一起。
generatePersistedQueryIdsFromManifest({loadManifest: () => import("./path/to/persisted-query-manifest.json"),})
最后,将 generatePersistedQueryIdsFromManifest
返回的链接与 ApolloClient
's HttpLink
连接在一起。将它们合并为单个链接的最简单方法是使用 concat
。
import { HttpLink, InMemoryCache, ApolloClient } from "@apollo/client";import { generatePersistedQueryIdsFromManifest } from "@apollo/persisted-query-lists";import { createPersistedQueryLink } from "@apollo/client/link/persisted-queries";const persistedQueryLink = createPersistedQueryLink(generatePersistedQueryIdsFromManifest({loadManifest: () => import("./path/to/persisted-query-manifest.json"),}),);const client = new ApolloClient({cache: new InMemoryCache(),link: persistedQueriesLink.concat(httpLink),});
将持久查询链接包含在您的客户端实例化中,您的客户端将发送您的清单中的操作ID,而不是整个操作字符串。
该 @apollo/persisted-query-lists
包包含您可以使用的其他辅助工具,以验证您的操作清单是否配置正确以及 在运行时生成操作 ID。运行时生成比从清单中检索操作 ID 要慢,但不需要将清单提供给客户端。
有关更多信息,请参阅该包的 README。
APQ 实现
用于 APQ 的持久查询 Apollo Link 包含在 @apollo/client
包中:
npm install @apollo/client
该链接需要SHA-256哈希函数,但并未包含该函数。这是因为为了避免将特定的哈希函数作为依赖项。开发者应选择最适合其需求和环境的SHA-256函数(同步或异步)。
如果您在应用程序中还没有基于SHA-256的哈希函数,请单独安装一个。例如
npm install crypto-hash
该链接要求使用 ApolloClient
's HttpLink
。最简单的方法是将它们连接成一个单独的链接。
import { HttpLink, InMemoryCache, ApolloClient } from "@apollo/client";import { createPersistedQueryLink } from "@apollo/client/link/persisted-queries";import { sha256 } from 'crypto-hash';const httpLink = new HttpLink({ uri: "/graphql" });const persistedQueriesLink = createPersistedQueryLink({ sha256 });const client = new ApolloClient({cache: new InMemoryCache(),link: persistedQueriesLink.concat(httpLink),});
就是这样!通过在客户端实例化时包含持久化查询链接,客户端会发送操作ID而不是整个操作字符串。这样一来,网络性能得到改善,但不会包含持久化查询提供的操作安全列表的安全性优势。
createPersistedQueryLink
选项
该 createPersistedQueryLink
函数接受一个配置对象:
sha256
:一个SHA-256哈希函数。可以是同步或异步。提供SHA-256哈希函数是必需的,除非您通过generateHash
定义了一个完全自定义的哈希方法。generateHash
:一个可选函数,该函数接受查询 document 和返回哈希值。如果提供,此自定义函数将覆盖默认的哈希方法,该方法使用提供的sha256
函数。如果没有提供,持久化查询链接将使用一个回退的哈希方法,该方法利用sha256
函数。useGETForHashedQueries
:设置为true
以在发送查询哈希版本时使用HTTPGET
方法(但不适用于 mutations)。GET
请求与@apollo/client/link/batch-http
不兼容。如果您想为非- mutation 查询使用
GET
(无论是否已哈希),请将useGETForQueries: true
选项传递给HttpLink
。如果您想对所有请求使用GET
,请将fetchOptions: {method: 'GET'}
传递给HttpLink
。disable
:一个函数,它接受一个ErrorResponse
(见下文)并返回一个布尔值以禁用该会话的任何未来 persisted queries。默认在PersistedQueryNotSupported
或HTTP 400或500错误时禁用。
ErrorResponse
可选 disable
函数提供的参数是一个具有以下键的对象:
操作
: 发生错误的操作
(包含查询
、变量
、操作名
和上下文
)。响应
: 操作的执行结果(包含数据
和错误
以及,如果从服务器发送,还有扩展
)。GraphQL错误
: GraphQL端点的错误数组。网络错误
: 在链路执行或服务器响应期间发生的任何错误。
注意: 网络错误
是从下联的错误
回调中获得的值。在大多数情况下,GraphQL错误
是最后next
调用结果中的错误字段。网络错误可以包含额外的字段,例如,在发生失败的HTTP状态码时,@apollo/link/http
中的GraphQL对象。在这种情况下,如果存在该属性,GraphQL错误
是别名于networkError.result.errors
)。
Apollo工作室
Apollo工作室支持接收和满足APQs。只需将此链接添加到您的客户端应用程序中,即可在使用Apollo工作室时提高网络响应时间。
协议
APQs由三部分组成:查询签名、错误响应和协商协议。
查询签名
APQs的查询签名通过客户端请求的扩展字段发送。这是一种独立的传输方式,可以随操作发送额外信息。
{operationName: 'MyQuery',variables: null,extensions: {persistedQuery: {version: 1,sha256Hash: hashOfQuery}}}
在发送自动持久查询时,客户端省略了通常存在的查询
字段,并转而发送一个包含上述持久查询
对象的扩展字段。哈希算法默认为查询字符串的sha256
哈希。
如果客户端需要注册哈希,查询签名将与上述相同,但包括完整的查询文本如下:
{operationName: 'MyQuery',variables: null,query: `query MyQuery { id }`,extensions: {persistedQuery: {version: 1,sha256Hash: hashOfQuery}}}
这种情况应在将新的查询引入应用程序时,在所有客户端上只发生一次。
错误响应
当后端接收到初始查询签名时,如果它找不到先前存储的哈希,它将发送以下响应签名:
{errors: [{ message: 'PersistedQueryNotFound' }]}
如果后端不支持 APQs,或者不想为特定客户端提供支持,它可以发送以下信息,以告知客户端停止尝试发送哈希:
{errors: [{ message: 'PersistedQueryNotSupported' }]}
协商协议
为了支持 APQs,客户端和服务器必须按照以下步骤进行协商:
幸福路径
- 客户端发送没有
查询
字段的查询签名 - 服务器根据哈希值查找查询,如果找到,则解析数据
- 客户端接收数据并完成请求
缺少哈希路径
- 客户端发送没有
查询
字段的查询签名 - 服务器根据哈希值查找查询,但没有找到
- 服务器返回NotFound错误响应
- 客户端向服务器发送哈希和查询字符串
- 服务器完成响应并保存查询字符串 + 哈希以供未来查找
- 客户端接收数据并完成请求
构建时生成
如果您想在浏览器中避免哈希,可以使用构建脚本来包含请求中的哈希,然后在运行操作时传递一个获取该哈希的函数。这对于像 GraphQL Persisted Document Loader 这样使用webpack在构建时生成哈希的程序来说效果很好。
如果您使用上述加载器,可以将 { generateHash: ({ documentId }) => documentId }
传递给 createPersistedQueryLink
调用。