設計一款“實踐派”的REST API

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Roy Thomas Fielding博士在其著名的論文Architectural Styles andthe Design of Network-based Software Architectures中,詳細描述了幾種常見的軟件架構風格,其中第5章Representational State Transfer就是大名鼎鼎的REST風格。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"隨着Web 2.0和微服務的興起,以及SOAP Web Services的沒落,REST API開始大行其道,在JSON緊湊、易讀、高效率的加持下,使得該API風格幾乎成爲現代Remoting通信技術的事實標準。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一萬個人眼中就有一萬個哈姆雷特。對REST API風格的的理解和應用,歷來存在許多分歧和五花八門的使用方式,出現了譬如“學院派“設計,一切都要遵循理論並與之嚴格對齊,當然也湧現了不少反範式的民間設計。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文結合實際的生產環境經驗,試圖說明如何設計一款“實踐派“的REST  API。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"以資源爲中心","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"和麪向RPC的 SOAP Web Services不同,REST  API的核心是資源(Resource),掌握了資源就相當於牽住了牛鼻子。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"資源是一個廣義的概念,可以是業務實體,也可以是一個事件或動作,資源一般用URI來描述。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"資源的分類及描述,一般和業務領域建模很相似。以電商行業爲例,資源可以是用戶(User)、商品(Product)、訂單(Order)、結算(Checkout)、運輸(Shipment)等;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"REST API的核心理念就是圍繞資源而進行的一系列操作及狀態變化。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"公共參數","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一款好的REST  API應該將公共參數和業務參數分離設計,並支持獨立變化。公共參數建議以header或query_string的方式來進行傳遞。常見的公共參數有:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"客戶端相關:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       app_key,後端下發給客戶端的唯一標識;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       app_secret,與app_key強相關,一般不在網絡中傳輸;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       platform,Android|iOS|Web等API使用方的平臺識別碼;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       channel,安裝包渠道編號,一般用於APP客戶端;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"設備相關:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       device_id,客戶端的設備號;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       device_os,客戶端的設備操作系統;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       device_version,客戶端的設備版本;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       androidid/imei/idfa,Android|iOS等客戶端的標識;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       device_mac,網卡物理地址;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用戶相關:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       uid,公司統一用戶ID;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"版本相關:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       app_version,APP正式版本;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       gray_version,APP灰度版本;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       internal_version,APP內部版本;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"地理相關:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       ip,外網IP地址,一般客戶端拿不到,需服務端從負載均衡獲取;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       longitude,GPS經度;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       latitude,GPS緯度;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"國際化相關:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       language,用戶的語言;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       country,用戶的國家;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       currency,用戶的貨幣;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"網絡相關:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       net_status,網絡狀態,如3G/4G/5G/Wifi等;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"安全相關:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       signature,API摘要,防止僞造請求,簽名最好加入app_secret增加破解難度;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       access_token,用戶token,防止僞造身份,一般由公司SSO下發且帶TTL;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       timestamp,時間戳,防止重放;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"命名風格","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從最佳實踐角度來說,URL命名風格建議遵循以下一些原則:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       以名詞命名。如/api/products,/api/orders,/api/users。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       以複數命名。這只是一種約定俗成的風格,便於潛在的擴展需要。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       需要有統一的API根。如/api/business1,/api/business2。這樣的好處是能做好望文生義,見到API即可瞭解該接口來自哪個服務,也便於監控和日誌統計。在微服務的架構設計準則下,API根尤爲重要。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       儘可能採用蛇形(snake_case)而不是駝峯(camelCase)來命名URL;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       採用HTTP Method來操作資源。通常採用GET來讀取資源,POST創建資源,PUT創建或全量更新資源,PATCH局部更新資源,DELETE刪除資源。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"值得注意的是,在實際使用場景中,我們並不是真的要堅持這些“理論派“,完全可以不必拘泥於這些約束,可以靈活變通,比如:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       有些場景沒法通過名詞來描繪 ,如用戶登錄。這時可以使用/api/users/login來命名;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       PUT和PATCH有相似之處,是否可以用PUT來代替PATCH?也未嘗不可,更有甚者,可以採用POST來代替;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"安全性","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不安全的REST  API會導致信息泄露,給不法分子可乘之機,嚴重的還會損害公司信譽和形象。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個安全的REST API需要具備哪些因素呢?這裏列出幾種比較實用的安全設計準則:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"採用HTTPS","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從現在開始,全體API就應該採用HTTPS傳輸。完全不用擔心由此帶來的資源消耗和耗時增加。API網關做SSL卸載所帶來的額外CPU開銷,完全可以通過增加更多的服務器,通過水平擴展來分攤壓力。而耗時的增加相對於業務層延遲來說,幾乎可以忽略不計,這種情況更多應該在應用層做耗時調優,而不用理會接入層的開銷。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"摘要(Digest)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"REST API摘要本質上是一種不可逆的指紋,目的是實現API請求不可篡改。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"REST API摘要有時候也被通俗的稱爲接口簽名,注意這裏和密碼學上的數字簽名嚴格上說意義不太一樣,數字簽名是指利用私鑰加密簽發,接收方用公鑰解密。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏介紹一種實用的摘要算法:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       API提供方(Provider)提供app_key和app_secret給API使用方(Consumer);","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       API使用方將請求參數和app_key、app_secret、時間戳(timestamp)等按一定的算法進行混合、排序,生成一個字符串;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       API使用方對上一步生成的字符串,按常見的摘要算法(如MD5、SHA-256等)進行哈希計算,得到指紋;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       API使用方將上一步得到的指紋,以signature=的形式,追加到請求參數中;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       API提供方接收到請求後,採用同樣的方式,進行哈希計算,再和signature進行比較,從而達到驗證簽名的目的;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"值得注意的是,app_secret在API使用方僅參與摘要計算,起到一個隨機鹽的作用,但不隨請求傳輸。這樣的好處是防止被截獲。另外API提供方已經存在app_secret,自然不需要請求方再次傳遞。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"好了,上面的摘要算法真的固若金湯、牢不可破嗎?答案是否定的,沒有絕對的安全。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最常見的威脅就是重放攻擊。儘管我們已經設置了timestamp時間戳,服務端也完全可以利用timestamp和當前時間的間隔來限制重複請求,但也會存在兩個問題:客戶端和服務端可能會存在時鐘不一致,另外在有效的時間窗口內依然可以重放。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"解決方案有多種,從本質上說應該業務層面做好冪等,限制重複請求帶來的副作用。另外也可以考慮服務端給調用方分配一個計數,並在內部記錄和校驗,該計數只可以遞增,每次攻擊者試圖重放API就必須遞增該計數,然而摘要算法是不公開的,這就使得重放無法實現。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外的問題就是如果API使用方的代碼被反編譯,摘要算法和app_secret被同時破解,黑客就可以隨意僞造API請求了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"解決方案是採用動態隨機鹽,由API提供方爲每個設備或用戶生成動態隨機鹽並週期性刷新,記錄到數據庫中進行校驗。即使代碼被破解,攻擊者也無計可施。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"認證(Authentication)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"REST  API的認證是爲了驗明用戶身份,實現用戶身份的不可抵賴。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一種比較實用的做法是採用令牌(access_token)機制。即用戶登錄後,由API提供方(通常是用戶或Passport中心)頒發一個令牌給使用方,該token安全級別較高,全局唯一,不可逆,可週期性更新或支持續借(renew)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當API調用方將access_token加入到API請求後,API提供方會將讀取該參數,並查詢後臺進行用戶身份確認,從而實現認證功能。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲什麼採用token機制呢?原因很簡單,用戶身份一般由用戶名和密碼構成,API客戶端不可能將這些信息頻繁傳到服務端,這會增加信息泄密的風險。而token是不可理解的密文,且可以更新,故安全級別能得到較大提升。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"鑑權(Authorization)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"僅身份認證是不夠的,REST  API的鑑權是爲了解決用戶權限問題。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假如沒有鑑權,則用戶可以操作任何資源,這顯然是不安全的。常見做法是檢查用戶的權限與角色,確認對資源的操作權限。如普通用戶只能刪除、修改自己發佈的內容,管理員則可以操作任何用戶的數據。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"國際化","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在一些國際化的業務中,如跨境電商,往往需要REST API支持國際化。常見的實用做法是API調用方採集用戶的國際化屬性,加入API請求參數。API提供方讀取這些屬性,存儲到後臺,或是直接在業務層面使用。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"常見的國際化屬性有:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       語言。通常會採用language = EN | ZH | FR 等形式進行參數傳遞;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       國家。通常採用country = CN | US | BR 等形式;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       貨幣。通常採用currency = USD | CNY | EUR 等形式;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       時區。通常採用time_zone = GMT+8:00形式;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"兼容性","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"兼容性是REST  API開發者的必備素質,很多線上迴歸錯誤(往往還很隱晦)都是由於API沒有考慮向下兼容造成的。因此需要考慮一些設計準則:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       只加字段,不刪除字段,不修改字段名稱;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       任何時候開發新功能,需要考慮到版本控制,即新功能只限於新版本,除非明確老版本也能使用而不受影響;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"冪等性","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"冪等性並不是絕對的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們考慮下HTTP 常見Method的冪等性,如GET /PUT/PATHCH/DELETE,這些操作天然具備冪等性語義,即多次操作,不會產生副作用。而POST操作,默認在多次操作下會多次創建資源,因此不是冪等的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然而,業務實現是靠技術人員實現的,因此完全可以人爲控制冪等性。所以,這裏並不建議過於教條,而應該根據實際情況來決定冪等性語義。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"過濾","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個設計良好的REST  API應該具備過濾能力。一種實用的做法就是通過傳業務參數來控制過濾邏輯,如id=123456,user_name=xxx等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外一種層面的過濾是指返回的字段,應該可以由API調用方來控制,如fields=user_name, user_age, user_city等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣做的好處是顯而易見的,一方面可以減少服務端的資源消耗,特別是存儲的查詢壓力,另一方面也可以減少網絡帶寬的佔用和API使用方的反序列化成本。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"排序","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"業務場景往往會有排序的訴求,如商品展示可以按發佈時間、熱度、銷售量進行排序。REST  API應該能支持靈活易用的排序方式。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一種常見的最佳實踐就是在API請求中增加sort字段,並且支持多維度排序。如按時間正向排序,且按積分反向排序,就可以表示成:sort=+time,-score。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"分頁","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"幾乎所有的產品形態都需要分頁,分頁功能看似簡單,實際上做起來很複雜。比如:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       如何保證每一頁的數據不存在重複或丟失?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       大數據量的集合(千萬級別),如何實現深度分頁,且效率不受影響?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       如何實現跳頁,即隨機讀取某一頁數據?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這些問題不在本文討論範圍內,感興趣的讀者可以自行搜尋答案。這裏介紹兩種常見的分頁請求參數方式。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       採用頁碼方式,如offset/limit或page_no/page_size。這種方式很常見,通常對於大的數據量來說,隨着頁碼的增加分頁效率會遞減。當然也可以採用一些優化的技巧,如MySQL採用覆蓋索引的子查詢;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       採用遊標方式,如cursor。這種方式相對簡單,適合單維度、固定排序的數據分頁。好處是時間複雜度可以是常量級別,弊端是不可以隨機讀取,只能從頭順序訪問,當然也可以通過其他方式拿到cursor直接去訪問下一頁;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"狀態碼","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"REST  API基於HTTP,自然需要定義響應的狀態碼,常見的預定義狀態碼大致可參考:","attrs":{}}]},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n

1XX

\n
\n

100-101

\n
\n

信息提示

\n
\n

2XX

\n
\n

200-206

\n
\n

成功

\n
\n

3XX

\n
\n

300-305

\n
\n

重定向

\n
\n

4XX

\n
\n

400-415

\n
\n

客戶端錯誤

\n
\n

5XX

\n
\n

500-505

\n
\n

服務器錯誤

\n
"}}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在實際REST API設計中,可以靈活運用這些狀態碼,當然也大可不必拘泥於這些預設的狀態碼並咬文嚼字。常見的以200和4XX、5XX使用較多。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除此之外,還可以自定義一些非保留狀態碼如6XX、7XX,用於一些特殊使用場景。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":" ","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"業務碼","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"業務碼是非常重要的信息表達方式,API使用方肯定不希望在出錯時,只是看到一個籠統的提示:“出錯了”,而是可以讀取到具體的錯誤碼和對應的提示。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常,REST API返回報文會使用一個固定的格式,常見的有:","attrs":{}}]},{"type":"embedcomp","attrs":{"type":"table","data":{"content":"\n\n\n\n
\n

“code”: “S0000000”,

\n

“message”: “api invoke success”,

\n

“data”:  {xxxxxx}

\n
"}}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中code和messaage就是業務碼和提示信息發揮作用的地方。我們可以定義一個業務碼字典和對應的提示信息。如E000001代表缺少參數xxx,E000002代表訂單已被刪除等等。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"謹記幾條原則:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       永遠不要把程序內部異常或錯誤拋給API調用方,而是採用業務碼優雅的提示;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"·       當輸出錯誤碼時,HTTP 狀態碼也應該同步調整,比如輸出5XX,這樣可以讓監控系統快速發現問題;","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":" ","attrs":{}}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章