踩坑: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.pswp.cn/bicheng/20206.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/20206.shtml
英文地址,請注明出處:http://en.pswp.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技術深度融合的新機遇。讓表單引擎…