GraphQL 是一种查询语言,用于从网站后端获取深度嵌套的结构化数据,类似于 MongoDB 查询。
请求通常是对某个通用 /graphql 端点的 POST,其正文如下:
但是,对于大型数据结构,这会变得低效 - 您在 POST 请求正文中发送大型查询,这(几乎总是)相同,并且仅在网站更新时发生变化; POST请求无法被缓存等。因此,开发了一个名为“持久查询”的扩展。这不是一个反抓取的秘密;而是一个秘密。您可以在此处阅读有关它的公共文档。
TLDR:客户端计算查询文本的 sha256 哈希值并仅发送该哈希值。此外,您可以将所有这些内容放入 GET 请求的查询字符串中,使其易于缓存。以下是来自 Zillow 的请求示例
如您所见,它只是有关 persistedQuery 扩展的一些元数据、查询的哈希值以及要嵌入查询中的变量。
这是来自 expedia.com 的另一个请求,以 POST 形式发送,但扩展名相同:
这主要优化了网站性能,但它给网页抓取带来了一些挑战:
因此,由于不同的原因,您可能会发现自己需要提取整个查询文本。您可以深入研究网站 JavaScript,如果幸运的话,您可能会在那里找到完整的查询文本,但通常,它是以某种方式从多个片段等动态构建的。
因此,我们想出了一个更好的方法:我们根本不碰客户端 JavaScript。相反,我们将尝试模拟客户端尝试使用服务器不知道的哈希的情况。因此,我们需要拦截浏览器发送的(有效)请求,并将哈希值修改为伪造的,然后再将其传递给服务器。
对于这个用例,存在一个完美的工具:mitmproxy,一个开源 Python 库,它可以拦截您自己的设备、网站或应用程序发出的请求,并允许您使用简单的 Python 脚本对其进行修改。
下载 mitmproxy,并准备一个 Python 脚本,如下所示:
import json def request(flow): try: dat = json.loads(flow.request.text) dat[0]["extensions"]["persistedQuery"]["sha256Hash"] = "0d9e" # any bogus hex string here flow.request.text = json.dumps(dat) except: pass
这定义了 mitmproxy 将在每个请求上运行的钩子:它尝试加载请求的 JSON 主体,将哈希修改为任意值,并将更新的 JSON 写入作为请求的新主体。
我们还需要确保将浏览器请求重新路由到 mitmproxy。为此,我们将使用名为 FoxyProxy 的浏览器扩展。它在 Firefox 和 Chrome 中均可用。
只需添加具有以下设置的路线:
现在我们可以使用以下脚本运行 mitmproxy:mitmweb -s script.py
这将打开一个浏览器选项卡,您可以在其中实时观看所有拦截的请求。
如果您转到特定路径并查看请求部分中的查询,您将看到一些垃圾值已替换了哈希值。
现在,如果您访问 Zillow 并打开我们尝试用于扩展的特定路径,然后转到响应部分,客户端会收到 PersistedQueryNotFound 错误。
Zillow 前端的反应是将整个查询作为 POST 请求发送。
我们直接从此 POST 请求中提取查询和哈希。为了确保 Zillow 服务器不会忘记此哈希,我们定期使用完全相同的查询和哈希运行此 POST 请求。这将确保即使服务器的缓存被清理或重置或网站发生更改,抓取工具也能继续工作。
结论
持久查询是 GraphQL API 的强大优化工具,通过最小化负载大小和启用 GET 请求缓存来增强网站性能。然而,它们也给网络抓取带来了重大挑战,主要是由于对服务器存储的哈希值的依赖以及这些哈希值可能无效的可能性。
使用 mitmproxy 拦截和操作 GraphQL 请求提供了一种有效的方法来显示完整的查询文本,而无需深入研究复杂的客户端 JavaScript。通过强制服务器响应 PersistedQueryNotFound 错误,我们可以捕获完整的查询负载并将其用于抓取目的。定期运行提取的查询可确保抓取器保持功能,即使发生服务器端缓存重置或网站发展也是如此。
以上是逆向工程 GraphQL persistedQuery 扩展的详细内容。更多信息请关注PHP中文网其他相关文章!