Learning from bad examples
我當(dāng)時正在幫助一個需要將物業(yè)管理系統(tǒng)中的房屋可用性與客戶網(wǎng)站集成的朋友。 幸運(yùn)的是,物業(yè)管理系統(tǒng)具有API。 而不幸的是,這一切都錯了。
這個故事的目的不是給二手系統(tǒng)帶來不好的廣告,而是分享不應(yīng)該開發(fā)的東西,以及在設(shè)計(jì)API時學(xué)習(xí)正確的方法。
任務(wù)
我朋友的客戶正在使用Beds24系統(tǒng)管理他們的財(cái)產(chǎn)清單,并在各種預(yù)訂系統(tǒng)(預(yù)訂,AirBnB等)之間保持可用性同步。 他們正在建立一個網(wǎng)站,并希望搜索機(jī)制僅顯示可用于所選日期和客人人數(shù)的屬性。 聽起來像是一項(xiàng)艱巨的任務(wù),因?yàn)锽eds24提供了與其他系統(tǒng)集成的API。 不幸的是,開發(fā)人員在設(shè)計(jì)時已經(jīng)犯了很多錯誤。 讓我們逐步解決這些錯誤,看看到底出了什么問題以及應(yīng)該如何解決。
一宗罪:請求正文格式
由于只對獲取客戶端屬性的可用性感興趣,因此僅對/ getAvailabilities調(diào)用感興趣。 即使這是獲取可用性的調(diào)用,但實(shí)際上這是一個POST請求,因?yàn)樽髡邲Q定接受過濾器作為JSON正文。 以下是所有可能參數(shù)的列表:
{
"checkIn": "20151001",
"lastNight": "20151002",
"checkOut": "20151003",
"roomId": "12345",
"propId": "1234",
"ownerId": "123",
"numAdult": "2",
"numChild": "0",
"offerId": "1",
"voucherCode": "",
"referer": "",
"agent": "",
"ignoreAvail": false,
"propIds": [
1235,
1236
],
"roomIds": [
12347,
12348,
12349
]
}
讓我遍歷JSON對象并解釋其參數(shù)出了什么問題。
· 日期checkIn,lastNight和checkOut使用YYYYMMDD格式設(shè)置。 將日期編碼為字符串時,絕對沒有理由不使用標(biāo)準(zhǔn)ISO 8601格式(YYYY-MM-DD),因?yàn)檫@是大多數(shù)開發(fā)人員和JSON解析器都理解并期望的廣泛采用的標(biāo)準(zhǔn)。 除此之外,lastNight字段似乎是多余的,因?yàn)樘峁┝薱heckOut,它總是比前一天晚一天。 請始終使用標(biāo)準(zhǔn)日期編碼格式,不要要求API用戶提供冗余數(shù)據(jù)。
· 所有Id以及numAdult和numChild字段都是數(shù)字,但被編碼為字符串。 在這種特殊情況下,似乎沒有理由將它們編碼為字符串。
· 我們具有以下字段對:roomId和roomIds以及propId和propIds。 因?yàn)榭梢允褂胷oomIds和propIds傳遞ID,所以具有roomId和propId屬性不僅是多余的,而且這里還存在類型問題。 請注意,roomId需要一個字符串,而roomIds需要一個數(shù)字?jǐn)?shù)組。 這可能會造成混亂,解析問題,并且意味著即使我們在談?wù)撓嗤臄?shù)據(jù),后端本身也會對字符串執(zhí)行某些操作,并對數(shù)字執(zhí)行某些操作。
請不要將開發(fā)人員犯此類愚蠢的錯誤,并嘗試使用標(biāo)準(zhǔn)格式,并注意冗余和字段類型。 不要只是將所有內(nèi)容包裝在字符串中。
二宗罪:響應(yīng)正文格式
如上一部分有關(guān)請求正文格式的說明,我們僅關(guān)注/ getAvailabilities調(diào)用。 這次讓我們看一下響應(yīng)主體格式,看看有什么問題。 請記住,我們有興趣獲取給定日期和許多客人可用的屬性的ID。 以下是相應(yīng)的請求和響應(yīng)正文:
請求:
{
"checkIn": "20190501",
"checkOut": "20190503",
"ownerId": "25748",
"numAdult": "2",
"numChild": "0"
}
響應(yīng):
{
"10328": {
"roomId": "10328",
"propId": "4478",
"roomsavail": "0"
},
"13219": {
"roomId": "13219",
"propId": "5729",
"roomsavail": "0"
},
"14900": {
"roomId": "14900",
"propId": "6779",
"roomsavail": 1
},
"checkIn": "20190501",
"lastNight": "20190502",
"checkOut": "20190503",
"ownerId": 25748,
"numAdult": 2
}
· ownerId和numAdult在響應(yīng)中突然變成數(shù)字,而不是在請求正文中為字符串。
· 沒有屬性列表。相反,屬性是使用roomId作為鍵的頂級對象。這意味著,為了獲取可用屬性的列表,我們需要遍歷所有對象,檢查是否存在某些參數(shù)(例如roomsavail),舍棄其他參數(shù)(例如checkIn,lastNight等)以獲取屬性列表。然后,我們需要檢查roomsavail屬性的值,如果該值大于0,則將該屬性視為可用。但是請稍等,在這里仔細(xì)查看:" roomsavail":" 0",在這里" roomsavail":1.看到模式了嗎?如果沒有可用的房間,則值為字符串,而如果有可用的房間,則為數(shù)字!這將在諸如JAVA之類的強(qiáng)制執(zhí)行類型安全的語言中引起很多問題,因?yàn)橥粚傩圆粦?yīng)屬于不同的類型。請使用適當(dāng)?shù)腏SON列表來顯示數(shù)據(jù)集合,而不要像上面那樣使用奇怪的鍵值構(gòu)造,并確保不要在對象之間更改字段類型。正確格式的響應(yīng)如下所示(請注意,這種格式也可以在不重復(fù)任何內(nèi)容的情況下獲取有關(guān)房間的信息):
{
"properties": [
{
"id": 4478,
"rooms": [
{
"id": 12328,
"available": false
}
]
},
{
"id": 5729,
"rooms": [
{
"id": 13219,
"available": false
}
]
},
{
"id": 6779,
"rooms": [
{
"id": 14900,
"available": true
}
]
}
],
"checkIn": "2019-05-01",
"lastNight": "2019-05-02",
"checkOut": "2019-05-03",
"ownerId": 25748,
"numAdult": 2
}
三宗罪:錯誤處理
此API中的錯誤處理是通過以下方式實(shí)現(xiàn)的:即使發(fā)生錯誤,所有請求也會返回200的響應(yīng)代碼。 這意味著除了解析正文并檢查是否存在error或errorCode字段之外,沒有其他方法可以區(qū)分響應(yīng)是成功還是失敗。 該API僅包含6個錯誤代碼,如下所示:

Error codes of Beds24 API
請考慮在出現(xiàn)問題時不要使用這種返回響應(yīng)代碼200(成功)的方法,除非這是在API框架中使用的標(biāo)準(zhǔn)方法。 優(yōu)良作法是使用大多數(shù)客戶端和開發(fā)人員都可以識別的標(biāo)準(zhǔn)HTTP錯誤代碼。 例如,以上屏幕快照中的錯誤代碼1009應(yīng)該替換為401(未經(jīng)授權(quán))HTTP代碼。 如果API客戶端可以預(yù)先知道是否解析主體以及如何解析主體(作為數(shù)據(jù)對象或錯誤對象),則將使工作變得更輕松。 如果錯誤是特定于應(yīng)用程序的,則最好在響應(yīng)正文中返回400(錯誤請求)或500(服務(wù)器錯誤)以及相應(yīng)的錯誤消息。
對于給定的API選擇哪種錯誤處理策略,只要確保它是一致的并符合廣泛采用的HTTP標(biāo)準(zhǔn)即可。 這將使我們的生活更輕松。
四宗罪:"準(zhǔn)則"
以下是文檔中API使用的"準(zhǔn)則":
使用API時請遵守以下準(zhǔn)則
1.呼叫應(yīng)設(shè)計(jì)為僅發(fā)送和接收所需的最少數(shù)據(jù)。
2.一次僅允許一個API調(diào)用,您必須等待第一個調(diào)用完成才能開始下一個API調(diào)用。
3.多個呼叫之間的間隔應(yīng)間隔幾秒鐘。
4. API調(diào)用應(yīng)盡量少用,并保持在合理的業(yè)務(wù)使用所需的最低限度內(nèi)。
5. 5分鐘內(nèi)過度使用將導(dǎo)致您的帳戶被封鎖,而不會發(fā)出警告。
6.我們保留自行決定禁用任何我們認(rèn)為過度使用API函數(shù)的訪問的權(quán)利,恕不另行通知。
雖然第1點(diǎn),第4點(diǎn)有意義,但我不同意其他觀點(diǎn)。 讓我解釋一下原因。
2.如果您正在構(gòu)建REST API,則應(yīng)假定該API是無狀態(tài)的,在任何時間點(diǎn)都不應(yīng)存在任何狀態(tài)。 這是REST在云應(yīng)用程序中有用的原因之一。 如果發(fā)生故障,可以自由地重新部署無狀態(tài)組件,并且它們可以根據(jù)負(fù)載變化進(jìn)行擴(kuò)展。 請確保在設(shè)計(jì)RESTful API時,它實(shí)際上是無狀態(tài)的,并且開發(fā)人員無需關(guān)心"一次請求"之類的事情。
3.這是一個模棱兩可,非常奇怪的準(zhǔn)則。 不幸的是,我無法弄清楚作者創(chuàng)建此"指南"的原因,但是它給人一種感覺,即在請求本身之外進(jìn)行了一些處理,因此,緊接彼此進(jìn)行調(diào)用可能會使系統(tǒng)處于某種錯誤狀態(tài) 。 同樣,作者說"幾秒鐘"的事實(shí)并沒有提供有關(guān)兩次請求之間實(shí)際時間的具體信息。
再次參見圖5和6。在這種情況下,沒有解釋什么是"過度"。 是每秒10個請求還是1個? 此外,某些網(wǎng)站將擁有大量流量,并且僅由于這些原因而阻止它們使用API,而不會發(fā)出任何警告,可能會使開發(fā)人員遠(yuǎn)離此類系統(tǒng)。 請?jiān)谥贫ù祟愔改蠒r具體說明,并在提出此類規(guī)則時考慮用戶。
五宗罪:文件
API文檔的外觀如下所示:

Beds24 API documentation look&feel
這里唯一的問題是可讀性和整體感覺。 如果作者使用markdown而不是自定義非樣式html,則同一文檔的外觀可能會更好。 為了這篇文章的緣故,我在Dilliger的幫助下不到2分鐘創(chuàng)建了一個更好的版本。 結(jié)果如下:

Styled version of the documentation
請使用工具創(chuàng)建API文檔。 對于簡單的文檔,上面的一個markdown文件就足夠了,而對于更大,功能更豐富的文檔,最好使用Swagger或Apiary之類的工具。
這是Beds24 API文檔的鏈接,適合那些想要自己看一下的人。
六宗罪:安全
API文檔規(guī)定了關(guān)于所有端點(diǎn)的以下內(nèi)容:
要使用這些功能,必須在菜單設(shè)置>>帳戶>>帳戶訪問中允許API訪問。
但是,實(shí)際上,任何人都可以請求獲取數(shù)據(jù),而無需為某些調(diào)用傳遞任何憑據(jù),例如獲取特定屬性的可用性。 在文檔的不同部分中對此進(jìn)行了說明:
大多數(shù)JSON方法都需要API密鑰才能訪問帳戶。 可以在菜單>>帳戶>>帳戶訪問中設(shè)置API代碼。
除了上面關(guān)于身份驗(yàn)證的溝通不暢之外,API密鑰實(shí)際上是用戶需要自己創(chuàng)建的東西(實(shí)際上是手動鍵入密鑰,絕不自動生成),并且其長度應(yīng)在16到64個字符之間。 允許用戶自己創(chuàng)建密鑰可能會導(dǎo)致非常不安全的密鑰(很容易猜到)或某些格式問題,因?yàn)榭梢栽趲粼O(shè)置的該字段中輸入任何字符串。 在最壞的情況下,它也可能成為SQL注入或其他類型攻擊的門戶。 請不要讓用戶創(chuàng)建API密鑰。 始終為他們提供不可變的自動生成的密鑰,并能夠在需要時使其無效。
對于那些實(shí)際經(jīng)過身份驗(yàn)證的請求,我們有一個不同的問題:身份驗(yàn)證令牌必須作為請求主體的一部分發(fā)送,如下所示(從文檔中可以看出):

Beds24 API authentication example
在請求正文中包含身份驗(yàn)證令牌意味著服務(wù)器將需要首先解析請求正文,提取密鑰,執(zhí)行身份驗(yàn)證,然后決定如何處理請求:是否執(zhí)行請求。 如果成功通過身份驗(yàn)證,則不會涉及任何開銷,因?yàn)闊o論如何都將解析該正文。 萬一驗(yàn)證失敗,服務(wù)器將完成上述所有工作,只是提取令牌,從而浪費(fèi)寶貴的處理時間。 相反,更好的方法是使用Bearer身份驗(yàn)證方案或類似方法將身份驗(yàn)證令牌作為請求標(biāo)頭發(fā)送。 這樣,服務(wù)器僅在成功認(rèn)證的情況下才需要解析請求主體。 使用諸如Bearer令牌之類的標(biāo)準(zhǔn)身份驗(yàn)證方案的另一個原因僅僅是因?yàn)榇蠖鄶?shù)開發(fā)人員都熟悉它。
罪過7:性能
最后但并非最不重要的一點(diǎn)是,請求完成平均需要1秒鐘多一點(diǎn)的時間。 在現(xiàn)代應(yīng)用中,這種延遲可能是不可接受的。 因此,在設(shè)計(jì)API時要考慮性能。
盡管上面解釋了API的所有問題,但它確實(shí)可以完成。 但是,對于開發(fā)人員來說,理解和實(shí)現(xiàn)它需要花費(fèi)數(shù)倍的時間,并且需要花費(fèi)大量時間來編寫更復(fù)雜的解決方案以解決瑣碎的問題。 因此,在發(fā)布API之前,請考慮讓開發(fā)人員實(shí)現(xiàn)您的API。 確保文檔完整,清晰且格式正確。 檢查您的資源名稱是否遵循約定,數(shù)據(jù)結(jié)構(gòu)是否正確,易于理解和使用。 另外,請注意安全性和性能,不要忘記正確執(zhí)行錯誤處理。 如果在設(shè)計(jì)API時將上述所有因素都考慮在內(nèi),那么就不需要像前面的示例中那樣奇怪的"準(zhǔn)則"。
如前所述,這篇文章的目的不是讓您永遠(yuǎn)不要使用Beds24或任何類似的系統(tǒng),因?yàn)樗鼈兊腁PI沒有正確實(shí)現(xiàn)。 目標(biāo)是通過分享一個不好的例子并解釋如何更好地完成軟件來提高軟件產(chǎn)品的質(zhì)量。 希望這篇文章可以使某人更多地關(guān)注軟件開發(fā)最佳實(shí)踐,并使軟件系統(tǒng)更好。 直到下一次!
(本文翻譯自Robert Konarskis的文章《How NOT to design APIs》,參考:https://blog.usejournal.com/how-not-to-design-restful-apis-fb4892d9057a)