踩坑:6年后为何不用GraphQL了?

GraphQL 是一项令人难以置信的技术,自从我在 2018 年首次开始将其投入生产以来,它就吸引了很多人的注意力。
在一大堆无类型的 JSON REST API 上构建了许多 React SPA 之后,我发现 GraphQL 是一股清新的空气。

然而,随着时间的推移,我有机会部署到更需要关注安全性性能可维护性等非功能性要求的环境中,我的观点发生了变化。在本文中,我想向您介绍为什么今天我不向大多数人推荐 GraphQL,以及我认为更好的替代方案。

安全性
从 GraphQL 诞生之初,很明显将查询语言暴露给不受信任的客户端会增加应用程序的攻击面。然而,需要考虑的攻击种类比我想象的还要多,缓解这些攻击是一项相当大的负担。以下是我多年来遇到的最糟糕的情况……

1、授权
这是 GraphQL 最广为人知的风险。
如果您向所有客户端公开一个完全自文档化的查询 API,您最好确保每个字段都针对当前用户进行了适当的授权,以适应正在获取该字段的上下文。最初授权对象似乎足够了,但这很快就会变得不够。


query {  
  user(id: 321) {  
    handle # 我可以查看用户的公开信息  
    email # 我不应该因为可以查看用户名,就能看到他们的个人信息。  
  }  
  user(id: 123) {  
    blockedUsers {  
      # 有时我甚至不应该看到他们的公开信息,  
      # 因为上下文很重要!  
      handle  
    }  
  }  
}

人们不禁要问,GraphQL 对“访问控制失效”升至OWASP 前 10 名中的第 1 名负有多大责任。

这里的一个缓解措施是通过与GraphQL 库的授权框架集成,使 API 默认安全。每个返回的对象和/或解析的字段,都会调用您的授权系统来确认当前用户是否具有访问权限。

将此与 REST 世界进行比较,一般来说,您会授权每个端点,这是一项小得多的任务。

2、速率限制
我刚刚针对一个非常受欢迎的网站的 GraphQL API 浏览器测试了此攻击:


query {  
  \_\_schema{  
    types{  
      \_\_typename  
      interfaces {  
        possibleTypes {  
          interfaces {  
            possibleTypes {  
              name  
            }  
          }  
        }  
      }  
    }  
  }  
}  

并在 10 秒后获得了 500 响应。我刚刚耗费了某人 10 秒的 CPU 时间来运行这个(删除空格)128 字节查询,而且它甚至不需要我登录。

这种攻击的常见缓解方法:

  • 估算解析数据结构schema中每个字段的复杂度,放弃超过某个最大复杂度值的查询

  • 捕捉运行查询的实际复杂度,并将其从按一定间隔重置的积分桶中提取出来

要正确计算复杂度是一件很复杂的事情。如果在执行前不知道返回列表字段的长度,计算就会变得特别棘手。您可以对这些字段的复杂性进行假设,但如果假设错误,最终可能会限制有效查询的速率或不限制无效查询的速率。

更糟糕的是,构成结构schema的图形通常包含循环。比方说,您运行一个博客,其中的每篇文章都有多个标签,您可以从中看到相关的文章。


type Article {  
  title: String  
  tags: \[Tag\]  
}  
type Tag {  
  name: String  
  relatedTags: \[Tag\]  
}  

在估算 Tag.relatedTags 的复杂度时,您可能会假设一篇文章永远不会有超过 5 个标签,因此将该字段的复杂度设为 5(或 5 * 其子字段的复杂度)。这里的问题是 Article.relatedTags 可以是它自己的子标签,因此您的估计不准确性会以指数形式增加。计算公式为 N^5 * 1:


query {  
  tag(name: "security") {  
    relatedTags {  
      relatedTags {  
        relatedTags {  
          relatedTags {  
            relatedTags { name }  
          }  
        }  
      }  
    }  
  }  
}  

您预计复杂度为 5^5 = 3,125。如果攻击者能找到一篇有 10 个标签的文章,他们就能触发一个 "真实 "复杂度为 10^5 = 100_000 的查询,比预计的复杂度高 20 倍。

部分缓解措施是防止深度嵌套查询。不过,上面的示例表明,这并不是真正的防御措施,因为这并不是一个异常深的查询。GraphQL Ruby 的默认最大深度是 13,而这只是 7。

与 REST 端点的速率限制相比,后者的响应时间通常相当。在这种情况下,你所需要的只是一个桶式速率限制器,防止用户在所有端点上的请求超过每分钟 200 次。如果确实有速度较慢的端点(如 CSV 报告或 PDF 生成器),可以为其定义更严格的速率限制。使用某些 HTTP 中间件,这一点非常简单:


Rack::Attack.throttle('API v1', limit: 200, period: 60) do |req|  
  if req.path =~ '/api/v1/'  
    req.env\['rack.session'\]\['session\_id'\]  
  end  
end  

3、查询解析
在执行查询之前,首先要对其进行解析。我们曾经收到过一份笔试报告,证明有可能伪造出一个无效的查询字符串,导致服务器宕机。例如

query {  
  \_\_typename @a @b @c @d @e ... # imagine 1k+ more of these  
}  

这是一个语法上有效的查询,但对我们的结构schema来说是无效的。符合规范的服务器会对其进行解析,并开始生成包含数千个错误的错误响应,我们发现这些错误所消耗的内存是查询字符串本身的 2,000 倍。由于这种内存放大效应,仅仅限制有效负载的大小是不够的,因为你会遇到比最小的危险恶意查询还要大的有效查询。

如果你的服务器提供了一个概念,即在放弃解析之前最多会出现多少次错误,那么这种情况就可以得到缓解。如果没有,您就必须自行解决。目前还没有与这种严重程度相当的 REST 攻击。

性能
说到 GraphQL 的性能,人们经常会说它与 HTTP 缓存不兼容。就我个人而言,这并不是一个问题。对于 SaaS 应用程序来说,数据通常是高度用户特定的,提供陈旧的数据是不可接受的,所以我没有发现自己错过了响应缓存(或缓存失效导致的错误......)。

我发现自己在处理的主要性能问题是...

1、数据获取和 N+1 问题
我认为这个问题如今已被广泛理解。简而言之:如果字段解析器命中一个外部数据源(如 DB 或 HTTP API),并且它嵌套在一个包含 N 个项的列表中,那么它将执行这些调用 N 次。

这并不是 GraphQL 独有的问题,实际上,严格的 GraphQL 解析算法已经让大多数库共享了一种通用的解决方案:Dataloader 模式。

但 GraphQL 的独特之处在于,由于它是一种查询语言,当客户端修改查询时,如果后端没有任何变化,这就会成为一个问题。因此,我发现最终不得不在各处防御性地引入 Dataloader 抽象,以防将来客户端最终在列表上下文中获取字段。这需要编写和维护大量的模板。

与此同时,在 REST 中,我们通常可以将嵌套的 N+1 查询上传到控制器,我认为这种模式更容易理解:


class BlogsController < ApplicationController  
  def index  
    @latest\_blogs = Blog.limit(25).includes(:author, :tags)  
    render json: BlogSerializer.render(@latest\_blogs)  
  end  
  
  def show  
    # No prefetching necessary here since N=1  
    @blog = Blog.find(params\[:id\])  
    render json: BlogSerializer.render(@blog)  
  end  

end

2、授权和 N+1 问题
还有更多的 N+1!

如果你按照之前的建议与库包的授权框架集成,那么你现在就有了一个全新的 N+1 问题需要处理。让我们继续前面的 X API 示例:


class UserType < GraphQL::BaseObject  
  field :handle, String  
  field :birthday, authorize\_with: :view\_pii  
end  
  
class UserPolicy < ApplicationPolicy  
  def view\_pii?  
    # 哦,不,我点击了数据库来获取用户的好友信息  
    user.friends\_with?(record)  
  end  
end  
query {  
  me {  
    friends { # returns N Users  
      handle  
      birthday # runs UserPolicy[**view**](/query/searchAction.shtml?query=view)\_pii? N times  
    }  
  }  
}  

这实际上比我们之前的例子更难处理,因为授权代码并不总是在 GraphQL 上下文中运行。例如,它可能在后台作业或 HTML 端点中运行。这意味着我们不能天真地使用 Dataloader,因为 Dataloader 需要在 GraphQL 中运行(无论如何,在 Ruby 实现中)。

根据我的经验,这实际上是性能问题的最大根源。我们经常会发现,我们的查询花费在授权数据上的时间比其他任何事情都多。同样,这个问题在 REST 世界中根本不存在。

我曾使用请求级全局等讨厌的方法来缓解这一问题,以便在策略调用中记忆缓存数据,但感觉并不好。

3、耦合
根据我的经验,在成熟的 GraphQL 代码库中,您的业务逻辑会被强制引入传输层。这是通过一系列机制实现的,其中一些我们已经讨论过:

  • 解决数据授权问题,在整个 GraphQL 类型中加入授权规则

  • 解决突变/参数授权问题,从而在整个 GraphQL 参数中加入授权规则

  • 解决解析器数据获取 N+1 的问题,从而将这一逻辑转移到 GraphQL 特定的数据加载器中

  • 利用(可爱的)中继连接模式,将数据获取逻辑转移到 GraphQL 特定的自定义连接对象中

所有这一切的最终结果是,要对应用程序进行有意义的测试,就必须在集成层进行广泛的测试,即运行 GraphQL 查询。我发现这样做会带来痛苦的体验。遇到的任何错误都会被框架捕获,从而导致阅读 JSON GraphQL 错误响应中的堆栈跟踪这一有趣的任务。

由于授权和 Dataloaders 的许多工作都是在框架内完成的,因此调试通常要困难得多,因为您想要的断点并不在应用程序代码中。

当然,同样,由于这是一种查询语言,您需要编写更多的测试来确认我们提到的所有参数和字段级别的行为是否正常工作。

复杂性
总的来说,我们所讨论的各种安全和性能问题的缓解措施都会大大增加代码库的复杂性。并不是说 REST 就没有这些问题(虽然它的问题肯定要少一些),只是 REST 解决方案对于后端开发人员来说,实施和理解起来通常要简单得多。

总结主要原因
以上就是我不喜欢 GraphQL 的主要原因。我还有一些其他的憎恶,但为了让这篇文章继续下去,我将在这里总结一下。

  • GraphQL 不鼓励破坏性更改,也不提供处理这些更改的工具。这就为那些控制着所有客户端的人增加了不必要的复杂性,他们不得不寻找变通办法。

  • 对 HTTP 响应代码的依赖在工具中随处可见,因此处理 200 可能意味着从一切正常到一切宕机的所有情况,这可能会相当恼人。

  • 在 HTTP 2+ 时代,在一次查询中获取所有数据往往不利于缩短响应时间,事实上,如果服务器没有并行化,与向不同服务器发送并行处理的请求相比,响应时间会更长。

替代方案
好了,废话少说。我有什么建议?如果符合下述条件:

  • 控制所有客户

  • 拥有 ≤ 3 个客户端

  • 有一个用静态类型语言编写的客户端

  • 在服务器和客户端上使用的语言>1 种2

您最好使用符合 OpenAPI 3.0+ 标准的 JSON REST API。

根据我的经验,如果您的前端开发人员喜欢 GraphQL 的主要原因是其自文档化的类型安全特性,那么我认为这将非常适合您。

自从 GraphQL 出现以来,这方面的工具已经有了很大改进;有很多生成类型化客户端代码的选项,甚至包括特定框架的数据获取库。

到目前为止,我的经验非常接近于 "我使用 GraphQL 的最佳部分,但没有 Facebook 所需的复杂性"。

与 GraphQL 一样,有几种实现方法...

1、首先,实现工具从类型化/类型提示服务器中生成 OpenAPI 规范。Python 中的  FastAPI和 TypeScript 中的  tsoa 就是这种方法的很好例子,这是我最有经验的方法,而且我认为它运行良好。

2、规范先行相当于 GraphQL 中的 "结构schema 先行"。规范先行工具会根据手写的规范生成代码。我不能说我曾经看着一个 OpenAPI YAML 文件,然后想 "我真想自己写这个",但最近发布的 TypeSpec 完全改变了一切。

有了 TypeSpec,就可以实现相当优雅的结构优先工作流程:

  1. 编写简洁易读的 TypeSpec 结构

  2. 从中生成 OpenAPI YAML 规范

  3. 为您选择的前端语言(如 TypeScript)生成静态类型的 API 客户端

  4. 为您的后端语言和服务器框架生成静态类型的服务器处理程序(例如,TypeScript + Express、Python + FastAPI、Go + Echo)

  5. 为处理程序编写可编译的实现,并确保其类型安全

这种方法不太成熟,但我认为大有可为。

https://www.jdon.com/73868.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/20206.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

mybatis用map接收返回对象,不想让数据类型为tinyint自动转换为boolean,如何处理

在 MyBatis 中&#xff0c;当使用 Map 来接收查询结果时&#xff0c;MyBatis 会根据列的数据类型自动选择合适的 Java 类型来映射这些值。默认情况下&#xff0c;如果数据库列是 TINYINT(1)&#xff0c;MyBatis 可能会错误地将其映射为 boolean&#xff0c;因为它经常被误解为只…

PPP认证两种:PAP和CHAP,两次握手和三次握手

CHAP&#xff08;Challenge-Handshake Authentication Protocol&#xff0c;质询握手认证协议&#xff09;的设计理念是增强网络认证过程的安全性。在CHAP的三次握手过程中&#xff0c;不直接传送用户的明文密码&#xff0c;以此来提高安全性&#xff0c;具体步骤如下&#xff…

开源大模型源代码

开源大模型的源代码可以在多个平台上找到&#xff0c;以下是一些知名的开源大模型及其源代码的获取方式&#xff1a; 1. **艾伦人工智能研究所的开放大语言模型&#xff08;Open Language Model&#xff0c;OLMo&#xff09;**&#xff1a; - 提供了完整的模型权重、训练代…

springboot结合mybatis使用多数据源的方式

背景 最近有一个需求&#xff0c;有两个库需要做同步数据&#xff0c;一个Doris库&#xff0c;一个mysql库&#xff0c;两边的表结构一致&#xff0c;这里不能使用navicat等工具提供的数据传输之类的功能&#xff0c;只能使用代码做同步&#xff0c;springboot配置多数据…

如何设置手机的DNS

DNS 服务器 IP 地址 苹果 华为 小米 OPPO VIVO DNS 服务器 IP 地址 中国大陆部分地区会被运营商屏蔽网络导致无法访问&#xff0c;可修改手机DNS解决。 推荐 阿里的DNS (223.5.5.5&#xff09;或 114 (114.114.114.114和114.114.115.115) 更多公开DNS参考&#xff1a; 苹果…

ESP32-C3模组上实现蓝牙BLE配网功能(1)

本文内容参考&#xff1a; 《ESP32-C3 物联网工程开发实战》 乐鑫科技 蓝牙的名字由来是怎样的&#xff1f;为什么不叫它“白牙”&#xff1f; 特此致谢&#xff01; 一、蓝牙知识基础 1. 什么是蓝牙&#xff1f; &#xff08;1&#xff09;简介 蓝牙技术是一种无线数据和…

【缓存】OS层面缓存设计机制

操作系统的缓存设计机制是计算机体系结构中的一个重要组成部分&#xff0c;旨在提高系统的性能&#xff0c;特别是通过减少对慢速存储设备&#xff08;如硬盘&#xff09;的访问次数来加速数据的读取和写入。 以下是一些常见的操作系统缓存设计机制&#xff1a; CPU缓存&…

web学习笔记(六十一)

目录 如何使用公共组件来编写页面 如何使用公共组件来编写页面 1.导入公共组件nav.vue import Catenav from "/components/nav.vue"; 2.在页面插入子组件 如果使用了setup语法糖此时就可以直接在页面插入 <Catenav ></Catenav>标签&#xff0c; …

.NET 快速重构概要1

1.封装集合 在某些场景中,向类的使用者隐藏类中的完整集合是一个很好的做法,比如对集合的 add/remove 操作中包 含其他的相关逻辑时。因此,以可迭代但不直接在集合上进行操作的方式来暴露集合,是个不错的主意。 public class Order { private int _orderTotal; private Li…

Camunda BPM架构

Camunda BPM既可以单独作为流程引擎服务存在,也能嵌入到其他java应用中。Camunda BPM的核心流程引擎是一个轻量级的模块,可以被Spring管理或者加入到自定义的编程模型中,并且支持线程模型。 1,流程引擎架构 流程引擎由多个组件构成,如下所示: API服务 API服务,允许ja…

逻辑回归分类算法

文章目录 算法推导 线性回归解决连续值的回归预测&#xff1b;而逻辑回归解决离散值的分类预测&#xff1b; 算法推导 逻辑回归可以看作是两部分&#xff0c;以0、1分类问题说明&#xff1b; 线性回归部分 对于一个样本 x i x_i xi​&#xff0c;有n个特征 x i ( 1 ) x_i^{(1)…

蒙自源儿童餐新品上市,引领健康美味新潮流

随着夏日的热烈与儿童节的欢乐氛围到来&#xff0c;蒙自源品牌隆重推出儿童餐新品&#xff0c;以“快乐不分大小&#xff0c;谁还不是个宝宝”为主题&#xff0c;为广大消费者带来一场健康与美味的盛宴。新品上市活动将于5月25日举行&#xff0c;蒙自源将以其独特的产品魅力和创…

install

目录 1、 install 1.1、 //creates form with validation 1.2、 onStepChanging: function (event, currentIndex, newIndex) { 1.3、 onFinishing: function (event, currentIndex) { 1.4、 //init inst

最新 HUAWEI DevEco Studio 调试技巧

最新 HUAWEI DevEco Studio 调试技巧 前言 在我们使用 HUAWEI DevEco Studio 编辑器开发鸿蒙应用时&#xff0c;免不了要对我们的应用程序进行代码调试。我们根据实际情况&#xff0c;一般会用到以下三种方式进行代码调试。 肉眼调试法注释排错调试法控制台输出法弹出提示法断…

【算法实战】每日一题:将某个序列中内的每个元素都设为相同的值的最短次数(差分数组解法,附概念理解以及实战操作)

题目 将某个序列中内的每个元素都设为相同的值的最短次数 1.差分数组&#xff08;后面的减去前面的值存储的位置可以理解为中间&#xff09; 差分数组用于处理序列中的区间更新和查询问题。它存储序列中相邻元素之间的差值&#xff0c;而不是直接存储每个元素的值 怎么对某…

STM32 入门教程(江科大教材)#笔记2

3-4按键控制LED /** LED.c**/ #include "stm32f10x.h" // Device headervoid LED_Init(void) {/*开启时钟*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_I…

关系数据库:关系运算

文章目录 关系运算并&#xff08;Union&#xff09;差&#xff08;Difference&#xff09;交&#xff08;Intersection&#xff09;笛卡尔积&#xff08;Extended Cartesian Product&#xff09;投影&#xff08;projection&#xff09;选择&#xff08;Selection&#xff09;除…

微信小程序中应用van-calendar时加载时间过长,以及设置min-data无效的问题解决

一、我们微信小程序中应用van-calendar时&#xff0c;如果没有设置min-data&#xff0c;那么页面的加载时间会非常长&#xff0c;所以&#xff0c;一定一定要配置min-data&#xff1b; 二、vue中min-data的写法是:min-data“new Date(2023, 0, 1)”&#xff0c;而在小程序中的写…

docker使用docker logs命令查看容器日志的几种方式

以下是如何使用docker logs命令的基本示例&#xff1a; docker logs [容器ID或名称]如果想要实时查看日志&#xff0c;可以加上-f参数&#xff0c;这样日志就会像使用tail -f命令一样实时输出。 docker logs -f [容器ID或名称]如果只想查看最近几行的日志&#xff0c;可以使用…

让表单引擎插上AI的翅膀-记驰骋表单引擎加入AI升级

让表单引擎插上AI的翅膀 随着科技的飞速发展&#xff0c;人工智能&#xff08;AI&#xff09;已经逐渐渗透到我们工作和生活的每一个角落。在数字化办公领域&#xff0c;表单引擎作为数据处理和流程自动化的重要工具&#xff0c;也迎来了与AI技术深度融合的新机遇。让表单引擎…