一把梭:REST API 全用 POST?
寫這篇文章的原因主要還是因爲 V2EX 上的這個貼子 [1],這個貼子中說——
“對接同事的接口,他定義的所有接口都是 post 請求,理由是 https 用 post 更安全,之前習慣使用 restful api ,如果說 https 只有 post 請求是安全的話?那爲啥還需要 get 、put 、delete ?我該如何反駁他。”
然後該貼中大量的回覆大概有這麼幾種論調:
1)POST 挺好的,就應該這麼幹,溝通少,
2)一把梭,早點幹完早點回家,
3)吵贏了又怎麼樣?工作而已,優雅不能當飯喫。
雖然評論沒有一邊倒,但是也有大量的人支持。然後,我在 Twitter 上嘲諷了一下,用 POST 幹一切就像看到了來你家裝修工人說,“老子幹活就是用釘子釘一切,什麼螺絲、螺栓、卡扣、插銷…… 通通不用,釘槍一把梭,方便,快捷,安全,幹完早回家…… 不過,還是有一些網友覺得用 POST 挺好的,而且可以節約時間。所以,正好,我在《我做系統架構的原則》[2] 中的 “原則五” 中反對 API 返回碼無論對錯全是 200 的返回碼,我專門寫下這一篇文章,以正視聽。
這篇文章主要分成下面這幾個部分:
-
爲什麼要用不同的 HTTP 動詞?
-
Restful 進行復雜查詢
-
幾個主要問題的迴應
1) 爲什麼 API 要 Restful,並符合規範?
2) 爲什麼 “過早優化” 不適用於 API 設計?
3) POST 更安全嗎?
4) 全用 POST 可以節省時間溝通少嗎?
5) 早點回家的正確姿勢 6) 工作而已,優雅不能當飯喫
1、爲什麼要用不同的 HTTP 動詞
編程世界通常來說有兩種邏輯:“業務邏輯” 和 “控制邏輯”。
-
業務邏輯。就是你實現業務需求的功能的代碼,就是跟用戶需求強相關的代碼。比如,把用戶提交的數據保存起來,查詢用戶的數據,完成一個訂單交易,爲用戶退款…… 等等,這些是業務邏輯
-
控制邏輯。就是我們用於控制程序運行的非功能性的代碼。比如,用於控制程序循環的變量和條件,使用多線程或分佈式的技術,使用 HTTP/TCP 協議,使用什麼樣數據庫,什麼樣的中間件…… 等等,這些跟用戶需求完全沒關係的東西。
網絡協議也是一樣的,一般來說,幾乎所有的主流網絡協議都有兩個部分,一個是協議頭,一個是協議體。協議頭中是協議自己要用的數據,協議體纔是用戶的數據。所以,協議頭主要是用於協議的控制邏輯,而協議體則是業務邏輯。
HTTP 的動詞(或是 Method)是在協議頭中,所以,其主要用於控制邏輯。
下面是 HTTP 的動詞規範,一般來說,REST API 需要開發人員嚴格遵循下面的標準規範(參看 RFC7231 章節 4.2.2 – Idempotent Methods[3])
其中, PUT
和 PACTH
都是更新業務資源信息,如果資源對象不存在則可以新建一個,但他們兩者的區別是, PUT
用於更新一個業務對象的所有完整信息,就像是我們通過表單提交所有的數據,而 PACTH
則對更爲 API 化的數據更新操作,只需要更需要更新的字段(參看 RFC 5789[4])。
當然,現實世界中,可能並不一定嚴格地按照數據庫操作的 CRUD 來理解 API,比如,你有一個登錄的 API /login
你覺得這個 API 應該是 GET
, POST
, PUT
還是 PATCH
? 登錄的時候用戶需要輸入用戶名和密碼,然後跟數據庫裏的對比(select 操作)後反回一個登錄的 session token,然後這個 token 作爲用戶登錄的狀態令牌。如果按上面表格來說,應該是 select 操作進行 GET
,但是從語義上來說,登錄並不是查詢信息,應該是用戶狀態的更新或是新增操作(新增 session),所以還是應該使用 POST
,而 /logout
你可以使用 DELETE
。這裏相說明一下,不要機械地通過數據庫的 CRUD 來對應這些動詞,很多時候,還是要分析一下業務語義。
另外,我們注意到,在這個表格的最後一列中加入了 “是否冪等” 的,API 的冪等對於控制邏輯來說是一件很重要的事。 所謂冪等,就是該 API 執行多次和執行一次的結果是完全一樣的,沒有副作用。
-
POST
用於新增加數據,比如,新增一個交易訂單,這肯定不能是冪等的 -
DELETE
用於刪除數據,一個數據刪除多次和刪除一次的結果是一樣的,所以,是冪等的 -
PUT
用於全部數更新,所以,是冪等的。 -
PATCH
用於局部更新,比如,更新某個字段 cnt = cnt+1,明顯不可能是冪等操作。
冪等這個特性對於遠程調用是一件非常關鍵的事,就是說,遠程調用有很多時候會因爲網絡原因導致調用 timeout,對於 timeout 的請求,我們是無法知道服務端是否已經是收到請求並執行了,此時,我們不能貿然重試請求,對於不是冪等的調用來說,這會是災難性的。比如像轉帳這樣的業務邏輯,轉一次和轉多次結果是不一樣的,如果重新的話有可能就會多轉了一次。所以,這個時候,如果你的 API 遵從了 HTTP 動詞的規範,那麼你寫起程序來就可以明白在哪些動詞下可以重試,而在哪些動詞下不能重試。如果你把所有的 API 都用 POST 來表達的話,就完全失控了。
除了冪等這樣的控制邏輯之外,你可能還會有如下的這些控制邏輯的需求:
-
緩存。通過 CDN 或是網關對 API 進行緩存,很顯然,我們要在查詢
GET
操作上建議緩存。 -
流控。你可以通過 HTTP 的動詞進行更粒度的流控,比如:限制 API 的請用頻率,在讀操作上和寫操作上應該是不一樣的。
-
路由。比如:寫請求路由到寫服務上,讀請求路由到讀服務上。
-
權限。可以獲得更細粒度的權限控制和審計。
-
監控。因爲不同的方法的 API 的性能都不一樣,所以,可以區分做性能分析。
-
壓測。當你需要壓力測試 API 時,如果沒有動詞的區分的話,我相信你的壓力測試很難搞吧。
-
…… 等等
也許,你會說,我的業務太簡單了,沒有必要搞這麼複雜。OK,沒有問題,但是我覺得你最差的情況下,也是需要做到 “讀寫分離” 的,就是說,至少要有兩個動詞, GET
表示是讀操作, POST
表示是寫操作。
2、Restful 複雜查詢
一般來說,對於查詢類的 API,主要就是要完成四種操作:排序,過濾,搜索,分頁。下面是一些相關的規範。參考於兩個我覺得寫的最好的 Restful API 的規範文檔,Microsoft REST API Guidelines[5],Paypal API Design Guidelines[6]。
-
排序。對於結果集的排序,使用
sort
關鍵字,以及{field_name}|{asc|desc},{field_name}|{asc|desc}
的相關語法。比如,某 API 需要返回公司的列表,並按照某些字段排序,如:GET/admin/companies?sort=rank|asc
或是GET/admin/companies?sort=rank|asc,zip_code|desc
-
過濾。對於結果集的過濾,使用
filter
關鍵字,以及{field_name}op{value}
的語法。比如:GET/companies?category=banking&location=china
。但是,有些時候,我們需要更爲靈活的表達式,我們就需要在 URL 上構造我們的表達式。這裏需要定義六個比較操作:=
,<
,>
,<=
,>=
,以及三個邏輯操作:and
,or
,not
。(表達式中的一些特殊字符需要做一定的轉義,比如:>=
轉成ge
)於是,我們就會有如下的查詢表達式:GET/products?$filter=name eq'Milk'andprice lt2.55
查找所有的價柗小於 2.55 的牛奶。 -
搜索。對於相關的搜索,使用
search
關鍵字,以及關鍵詞。如:GET/books/search?description=algorithm
或是直接就是全文搜索GET/books/search?key=algorithm
。 -
分頁。對於結果集進行分頁處理,分頁必需是一個默認行爲,這樣不會產生大量的返回數據。
-
使用
page
和per_page
代表頁碼和每頁數據量,比如:GET/books?page=3&per_page=20
。 -
可選。上面提到的
page
方式爲使用相對位置來獲取數據,可能會存在兩個問題:性能(大數據量)與數據偏差(高頻更新)。此時可以使用絕對位置來獲取數據:事先記錄下當前已獲取數據裏最後一條數據的ID
、時間
等信息,以此獲取 “該 ID 之前的數據” 或 “該時刻之前的數據”。示例:GET/news?max_id=23454345&per_page=20
或GET/news?published_before=2011-01-01T00:00:00Z&per_page=20
。
另外,對於一些更爲複雜的操作,建議通過分別調用多個 API 的方式來完成,雖然這樣會增加網絡請求的次數,但是這樣的可以讓後端程序和數據耦合度更小,更容易成爲微服務的架構。
最後,如果你想在 Rest 中使用像 GraphQL 那樣的查詢語言,你可以考慮一下類似 OData[7] 的解決方案。OData 是 Open Data Protocol 的縮寫,最初由 Microsoft 於 2007 年開發。它是一種開放協議,使您能夠以簡單和標準的方式創建和使用可查詢和可互操作的 RESTful API。
3、幾個主要問題的迴應
下面是對幾個問題的直接回應
1)爲什麼 API 要 Restful,並符合規範?
Restful API 算是一個 HTTP 的規範和標準了,你要說是最佳實踐也好,總之,它是一個全世界對 HTTP API 的一個共識。在這個共識上,你可以無成本地享受很多的技術紅利,比如:CDN,API 網關,服務治理,監控…… 等等。這些都是可以讓你大幅度降低研發成本,避免踩坑的原因。
2)爲什麼 “過早優化” 不適用於 API 設計?
因爲 API 是一種契約,一旦被使用上,就很難再變更了,就算你發行新的版本的 API,你還要驅動各種調用方升級他們的調用方式。所以,接口設計就像數據庫模式設計一下,一旦設計好了,未來再變更就比較難了。所以,還是要好好設計。正如前面我給的幾個文檔——Microsoft REST API Guidelines[5],Paypal API Design Guidelines[6] 或是 Google API Design Guide[8] 都是讓你好好設計 API 的不錯的 Guidelines.
3)POST 更安全嗎?
不會。
很多同學以爲 GET
的請求數據在 URL 中,而 POST
的則不是,所以以爲 POST
更安全。不是這樣的,整個請求的 HTTP URL PATH 會全部封裝在 HTTP 的協議頭中。只要是 HTTPS,就是安全的。當然,有些網關如 nginx 會把 URL 打到日誌中,或是會放在瀏覽器的歷史記錄中,所以有人會說 GET
請求不安全,但是, POST
也沒有好到哪裏去,在 CSRF[9] 這個最常見的安全問題上,則完全就是針對 POST
的。安全是一件很複雜的事,無論你用哪方法或動詞都會不能代表你會更安全。
另外,
4)全用 POST 可以節省時間減少溝通嗎?
不但不會,反而更糟糕。
說這種話的人,我感覺是不會思考問題。
-
其一,爲 API 賦於不同的動詞,這個幾乎不需要時間。把 CRUD 寫在不同的函數下也是一種很好的編程風格。另外現在幾乎所有的開發框架都支持很快速的 CRUD 的開發,比如 Spring Boot,寫數據庫的 CRUD 基本上就不需要寫 SQL 語言相關的查詢代碼,非常之方便。
-
其二,使用規範的方式,可以節約新加入團隊人員的學習成本,而且可以大大減少跨團隊的溝能成本。規範和標準其實就是在節約團隊時間提升整體效率的,這個我們整個人類進行協作的基礎。所以,這個世界上有很多的標準,你只要照着這個標準來,你的所生產的零件就可以適配到其它廠商的產品上。而不需要相互溝通。
-
其三,全用 POST 接口一把梭,不規範不標準,使用你的這個山寨 API 的人就得來不斷的問你,反而增加了溝通。另外,也許你開發業務功能很快了,但是你在做控制邏輯的時候,你就要返工了,從長期上來講,你的欠下了技術債,這個債反而導致了更大的成本。
5)早點回家的正確姿勢
不要以爲你回家早就沒事了,如果你的代碼有這樣那樣的問題,別人看懂,或是出誤用了你的代碼出了問題,那麼,你早回家有什麼意義呢?你一樣要被打擾,甚至被叫到公司來處理問題。所以,你應該做的是爲了 “長期的早回家”,而不是 “短期的早回家”,要像長期的早回家,通常來說是這樣的:
-
把代碼組織設計好,有更好的擴展性。這樣在面對新需求的時候,你就可以做到少改代碼,甚至不改代碼。這樣你纔可能早回家。不然,每次需求一來,你得重新寫,你怎麼可能早回家?
-
你的代碼質量是不錯的,有不錯的文檔和註釋。所以,別人不會老有問題來找你,或是你下班後,叫你來處理問題。甚至任何人都可以很容易地接手你的代碼,這樣你纔可能真正不被打擾
6)工作而已,優雅不能當飯喫
迴應兩點:
其一,遵循個規範而已,把 “正常” 叫“優雅”,可見標準有多低。這麼低的標準也只能“爲了喫飯而生存了”。
其二,作爲一個 “職業程序員”,要學會熱愛和尊重自己的職業,熱愛自己職業最重要的就是不要讓外行人看扁這個職業,自己都不尊重這個職業,你讓別人怎麼尊重?尊重自己的職業,不僅僅只是能夠獲得讓人羨慕的報酬,而更是要讓自己的這個職業的更有含金量。
希望大家都能尊重自己從事的這個職業,成爲真正的職業化的程序員,而不是一個碼農!
參考資料
[1] https://www.v2ex.com/t/830030?p=1
[2] https://coolshell.cn/articles/21672.html
[3] https://www.rfc-editor.org/rfc/rfc7231#section-4.2.2
[4] https://tools.ietf.org/html/rfc5789
[5] https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md
[6] https://github.com/paypal/api-standards/blob/master/api-style-guide.md
[7] https://www.odata.org/
[8] https://cloud.google.com/apis/design
[9] https://en.wikipedia.org/wiki/Cross-siterequestforgery
作者:陳皓
來源:https://coolshell.cn/articles/22173.html
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/dt-3GCJek5dNgxMhOBLuKA