RESTful Service API 設(shè)計(jì)最佳工程實(shí)踐和常見(jiàn)問(wèn)題解決方案

前面兩篇內(nèi)容(RESTful Web Service 架構(gòu)剖析HTTP Methods 和 RESTful Service API 設(shè)計(jì))介紹了 RESTful Service 的基礎(chǔ)概念和理論知識(shí),本篇內(nèi)容打算總結(jié) RESTful Service APIs 設(shè)計(jì)最佳工程實(shí)踐和常見(jiàn)問(wèn)題的解決方案,側(cè)重點(diǎn)是幫助讀者更加有效的解決實(shí)際工程問(wèn)題和如何快速設(shè)計(jì)一套優(yōu)秀易用的 APIs。

為了有個(gè)討論的標(biāo)準(zhǔn)和設(shè)計(jì)目標(biāo),我們先來(lái)定義下一套優(yōu)秀的的 RESTful APIs 應(yīng)該是什么樣子:

  • 盡可能的遵守有關(guān) WEB 規(guī)范和常見(jiàn)約定;
  • 調(diào)用接口簡(jiǎn)單明了,可讀性強(qiáng),沒(méi)有歧義;
  • 不同 API 風(fēng)格保持一致,調(diào)用規(guī)則,傳入?yún)?shù)和返回?cái)?shù)據(jù)有統(tǒng)一的標(biāo)準(zhǔn);
  • 能夠?yàn)榭蛻舳颂峁┖?jiǎn)單靈活的數(shù)據(jù)訪問(wèn)方式;
  • 有一定的容錯(cuò)性和防止非法參數(shù)功能;
  • 高效,安全可靠,容易擴(kuò)展。

[長(zhǎng)文預(yù)警]閱讀本文可能需要半個(gè)小時(shí)以上,如果你沒(méi)有時(shí)間從頭讀到尾,可以先收藏或看看目錄,遇到你關(guān)注的問(wèn)題再詳細(xì)看起。

  1. API 命名應(yīng)該采用約定俗成的方式,保持簡(jiǎn)潔明了;
  2. 考慮到系統(tǒng)迭代和兼容性需求,API 中應(yīng)該引入版本規(guī)則;
  3. 優(yōu)雅的設(shè)計(jì)條件過(guò)濾,排序,搜索等傳入?yún)?shù)形式;
  4. 合理設(shè)計(jì)返回?cái)?shù)據(jù)的形式,格式和考慮啟用壓縮(gzip);
  5. 根據(jù)不同的 API 操作,設(shè)置合適的 HTTP 狀態(tài)碼和必要的出錯(cuò)信息;
  6. 使用 token 機(jī)制設(shè)計(jì)鑒權(quán)和驗(yàn)證系統(tǒng)(Authorization and Authentication[1])
  7. 如何實(shí)現(xiàn)數(shù)據(jù)的分頁(yè)返回;
  8. 如何處理有關(guān)聯(lián)資源的返回?cái)?shù)據(jù);
  9. 考慮啟用 HTTP 緩存機(jī)制;
  10. 限制 API 調(diào)用頻次(Rate limiting);
  11. 盡可能的使用 HTTPS,涉及用戶驗(yàn)證的 API 一定要強(qiáng)制啟用 HTTPS。

<br />

1.API 命名應(yīng)該采用約定俗成的方式,保持簡(jiǎn)潔明了

API 應(yīng)該采用簡(jiǎn)單明了和約定俗成的命名方式

簡(jiǎn)單明了意味著能消除歧義,更少出錯(cuò)和能夠減少不必要的文檔記錄。

所有 API 應(yīng)該使用 REST 架構(gòu)約定形式命名。REST架構(gòu)的思想是將 API 請(qǐng)求對(duì)象看成一個(gè)個(gè)資源,實(shí)現(xiàn)者使用相應(yīng)的 HTTP 的動(dòng)詞(GET, POST, PUT, PATCH, DELETE)來(lái)訪問(wèn)和操作這些資源。這些具體動(dòng)詞的意義和使用方法可以參見(jiàn)本系列的前一篇文章。
為了使 API 看上去簡(jiǎn)單明了,可讀性強(qiáng),我們一般使用名詞,而不是動(dòng)詞來(lái)命名這些資源。比如下面這些都是糟糕的設(shè)計(jì):

  • /getAllUser[2]
  • /setUserComments
  • /DeleteUserForId

之所以糟糕,不僅僅是它們顯得拖沓冗長(zhǎng),最重要的是,使用這樣的風(fēng)格和名字沒(méi)有固定的形式,不同的開(kāi)發(fā)者往往需要閱讀你的文檔才能開(kāi)始使用,也沒(méi)有充分利用HTTP Method,何況使用自己的動(dòng)詞可能會(huì)產(chǎn)生和HTTP Method沖突的情況。使用 REST 風(fēng)格的優(yōu)秀設(shè)計(jì)應(yīng)該像下面這些:

- GET /users  獲取所有用戶
- GET /users/1234  獲取ID為1234的用戶
- POST /users  創(chuàng)建一個(gè)新用戶
- PUT /users/1234  更新ID為1234的用戶
- PATCH /users/1234  更新ID為1234的用戶的部分內(nèi)容
- DELETE /users/1234  刪除ID為1234的用戶

這些API所有的操作只有一個(gè)節(jié)點(diǎn) /users,顯得簡(jiǎn)潔明了,如果熟悉 HTTP Method 的開(kāi)發(fā)者,一眼看上去就能猜到應(yīng)該如何使用。

您可能已經(jīng)注意到了,以上 API 中資源命名都使用了復(fù)數(shù)的形式。這是一個(gè)約定,它可以省去設(shè)計(jì)時(shí)考慮數(shù)據(jù)具體細(xì)節(jié)的麻煩(數(shù)據(jù)是復(fù)數(shù)還是單數(shù)?)?,F(xiàn)在大很多常見(jiàn)的系統(tǒng)都使用了復(fù)數(shù)形式。比如Twitter 的 REST APIs 和 Facebook 的 Graph API 基本都是復(fù)數(shù)形式。

然而實(shí)際系統(tǒng)一般都不可能只有單一資源,資源和資源之間有各種關(guān)系是很正常的情況,那么如何設(shè)計(jì)存在關(guān)聯(lián)資源(數(shù)據(jù))的API呢?

如果要設(shè)計(jì)一個(gè)資源擁有另外一個(gè)資源的情況的API,例如,設(shè)計(jì)一個(gè)包含用戶(users)和用戶的評(píng)論(comments)的 API 可以采用這樣的形式:

- GET /users/1234/comments  獲取用戶ID為1234的所有評(píng)論
- GET /users/1234/comments/1 獲取用戶ID為1234的評(píng)論ID為1的單個(gè)評(píng)論
- DELETE /users/1234/messages/1  刪除用戶評(píng)論ID為1,屬于用戶1234的單個(gè)評(píng)論

當(dāng)然,如果一個(gè)資源并不依附其它資源而可以獨(dú)立存在,是沒(méi)有必要這樣設(shè)計(jì)的,完全可以使用和 users 一樣的形式提供,如果要查詢(xún)其中的關(guān)系,可以使用其它資源作為 ID 的形式來(lái)過(guò)濾。例如 /comments?user_id=1234。關(guān)于這點(diǎn)詳細(xì)內(nèi)容可以參見(jiàn)下面的第三條“優(yōu)雅的設(shè)計(jì)條件過(guò)濾,排序,搜索和限制返回?cái)?shù)據(jù)的參數(shù)形式”。

上述設(shè)計(jì)原則都是使用 HTTP Method,會(huì)不會(huì)有超出 HTTP Method 表達(dá)語(yǔ)義的 API 呢?答案是肯定的。實(shí)際工程實(shí)踐中往往會(huì)遇到并不是對(duì)一個(gè)資源簡(jiǎn)單的 CRUD 的場(chǎng)景,設(shè)計(jì)此類(lèi) API 有這些手法可供參考:

  1. 將這些操作變成一個(gè)資源的屬性,比如 disable 一個(gè) user,可以在 user 里面加一個(gè) disabled 的屬性,可以設(shè)計(jì)一個(gè) API 使用 PATCH /users/1234 將 disabled 設(shè)置成 true 即可。

  2. 將這個(gè)操作看成某個(gè)資源的附屬資源(就像上面例子中的 comments 一樣)來(lái)設(shè)計(jì),比如GitHub的Star a gist API ,就是這樣的,它把star操作放在這個(gè)資源的后面,看上去好像是一個(gè)附屬資源:
    - PUT /gists/:id/star
    - DELETE /gists/:id/star

  3. 在不得不使用其它例外形式設(shè)計(jì) API 時(shí),盡量用文檔寫(xiě)清楚輸入輸出和返回值等其他必要信息,避免讓習(xí)慣了使用資源名的調(diào)用者感到困惑。
    例如,如果要設(shè)計(jì)一個(gè) API 用來(lái)根據(jù)輸入關(guān)鍵詞返回搜索結(jié)果,搜索結(jié)果可能有 user,可能有 comments,或者二者都有,這種情況下,我們很難按照約定的資源形式設(shè)計(jì)API。我們可以使用 GET /search 這樣的形式設(shè)計(jì) API,但是最好給出文檔說(shuō)明,說(shuō)明輸入和輸出細(xì)節(jié)。

<br />

2.考慮到系統(tǒng)迭代和兼容性,需要在 API 中引入版本規(guī)則

考慮到系統(tǒng)迭代和兼容性,需要在 API 中引入版本規(guī)則

現(xiàn)代系統(tǒng)的迭代速度一般都很快,設(shè)計(jì)優(yōu)良的 API 版本規(guī)則可以給持續(xù)集成和系統(tǒng)升級(jí)帶來(lái)便利,降低因系統(tǒng)迭代引發(fā)的問(wèn)題。在升級(jí)到新版 API 到同時(shí),可以選擇依然支持舊版本 API 一段時(shí)間,這樣可以給其它客戶端和子系統(tǒng)一個(gè)緩沖時(shí)間,讓其有充分的時(shí)間升級(jí)和適配新版本的API。

關(guān)于設(shè)置API的版本信息,常見(jiàn)的有兩種方法,一種是將版本號(hào)放在 http header 內(nèi),另一種是直接放在 URL 中。而放在 URL 中是最常見(jiàn)的做法,比如:
- GET https://api.twitter.com/1.1/friends
- GET "https://graph.facebook.com/v2.8/me

其中1.1 和v2.8就是API的版本號(hào),這種做法的好處是簡(jiǎn)單易讀,不容易混淆。
為了簡(jiǎn)單起見(jiàn),可以省略最新的 API 版本號(hào),假設(shè)v3.0是最新版本,調(diào)用下面的API應(yīng)該返回相同的結(jié)果:

- /api/users/1234
- /api/v3.0/users/1234
- /v3/users/1234

如果一個(gè) API 的版本過(guò)期了,任何把該請(qǐng)求重定向到最新版本上。比如 user API v1 版本過(guò)期了,當(dāng)有調(diào)用/api/v1.0/users/1234的時(shí)候,應(yīng)該被重定向(http 30x)到最新的 /api/v2.0/users/1234 上。

<br />

3.優(yōu)雅的設(shè)計(jì)條件過(guò)濾,排序,搜索等傳入?yún)?shù)形式

優(yōu)雅的設(shè)計(jì)條件過(guò)濾,排序,搜索等傳入?yún)?shù)形式

RESTful API 經(jīng)常有對(duì)返回?cái)?shù)據(jù)過(guò)濾和排序的要求,這些輸入?yún)?shù)推薦采用 HTTP Query Parameter 的方式實(shí)現(xiàn)。

  • 比如你要設(shè)計(jì)一個(gè)API,返回所有已經(jīng)登錄的用戶,可以這樣做:
    GET /users?login=true

  • 獲取所有的用戶,返回結(jié)果按照create_at降序排序可以這樣設(shè)計(jì):
    GET /users?sort=-create_at

  • 當(dāng)然也可以組合使用過(guò)濾條件和排序:
    GET /users?sort=-create_at,login_at&login=true 表示返回所有已登錄用戶,結(jié)果按照create_at降序, login_at升序

  • 有些時(shí)候你可以單獨(dú)為 API 設(shè)計(jì)一個(gè) Query Parameter 專(zhuān)門(mén)用于搜索。這樣特別適用你的后端在使用了ElasticSearch 或者其它如 Lucene,Solr之類(lèi)的搜索引擎架構(gòu),因?yàn)閺?API 中傳遞過(guò)來(lái)的 Query Parameter 可以直接設(shè)置成這些搜索框架的輸入條件。這種情況的API可以這樣設(shè)計(jì):
    GET /users?q=key&&sort=-create_at,login_at&diabled=false

  • 對(duì)于一些常用的條件搜索和過(guò)濾,可以考慮映射到一個(gè)新的API(相當(dāng)于快捷方式)比如設(shè)計(jì)一個(gè)用于返回最近登錄用戶的API:
    GET /users/recently_login
    這種設(shè)計(jì)可以簡(jiǎn)化客戶端的調(diào)用,否則調(diào)用者每次都要根據(jù)時(shí)間合成 Query Parameter,增加了客戶端使用復(fù)雜度。

  • 查詢(xún)數(shù)據(jù)的部分內(nèi)容
    有些時(shí)候資源屬性很多(比如 user 包含 name, address, email, phone...),不同的客戶端需要的內(nèi)容不盡相同(有的客戶端可能只需要name, address),如果一股腦的全部返回,尤其在數(shù)據(jù)量比較大情況下會(huì)對(duì)帶寬帶來(lái)不必要的浪費(fèi)。我們可以采用這樣的形式來(lái)過(guò)濾數(shù)據(jù)的屬性:
    GET /user?fields=id,user_name,address&diabled=false&sort=-login_at
    GET /facebook/v2.8/me?fields=id,name,birthday,cover,devices,email&access_token=xxx

您可以已經(jīng)注意到了上述API中都使用了下劃線(user_name)的形式來(lái)命名這些參數(shù)。作為程序員你一定會(huì)爭(zhēng)論是使用劃線(user_name)還是使用駝峰(userName)的形式呢?

這個(gè)問(wèn)題一直沒(méi)有一個(gè)明確的答案。一般要求所有 API 保持風(fēng)格一致即可。從個(gè)人接觸的一些常見(jiàn)系統(tǒng) API 來(lái)看,使用下劃線的方式居多。值得提到的是有項(xiàng)研究表明,使用下劃線分割的形式比使用駝峰的形式更容易閱讀(容易20%),如果從可讀性方面來(lái)說(shuō)應(yīng)該使用下劃線的方式來(lái)分隔是個(gè)不錯(cuò)的選擇。

<br />

4.合理設(shè)計(jì)返回?cái)?shù)據(jù)的形式,格式和考慮啟用壓縮(gzip)

返回?cái)?shù)據(jù)的形式,格式和啟考慮用壓縮(gzip)

GET 操作的返回?cái)?shù)據(jù)是顯而易見(jiàn),這里不做過(guò)多討論。對(duì)于更新和創(chuàng)建操作(PUT POST PATCH),API 在執(zhí)行相關(guān)的操作之后要把更新后的數(shù)據(jù)也做為返回值的一部分返回給調(diào)用者,這樣可以避免調(diào)用者再次調(diào)用 GET API 來(lái)獲取更新,而浪費(fèi)一次 HTTP 請(qǐng)求。特別是對(duì)于 POST 操作的 API,因?yàn)樵?API 會(huì)創(chuàng)建數(shù)據(jù),該數(shù)據(jù)被創(chuàng)建后的唯一性 ID 往往由服務(wù)端生成,如果不返回新創(chuàng)建的 ID,客戶端就不能基于這個(gè)數(shù)據(jù)做進(jìn)一步操作。這個(gè)部分理論基礎(chǔ)可以參見(jiàn)RESTful Web Service 架構(gòu)剖析 - 6.2 Resource Identifiers。

舉個(gè)例子來(lái)說(shuō)明這個(gè)情況:
假如有個(gè)系統(tǒng)提供一個(gè) API 用于上傳一張圖,這張圖上傳之后你可以調(diào)用另外一個(gè) API 修改這個(gè)圖片的描述。如果調(diào)用上傳 API 后,返回?cái)?shù)據(jù)中沒(méi)有返回這張圖的唯一性 ID,你就無(wú)法接著調(diào)用其它 API 引用到這個(gè)圖的資源,從而無(wú)法進(jìn)行修改描述的操作,除非之前額外再次調(diào)用查詢(xún)操作拉取到這張圖唯一性 ID。

通常,POST 操作成功以后,我們一般也把新創(chuàng)建的資源的 URL 放在 HTTP header 的 location 字段中,方便客戶的拉取。例如上上樹(shù)圖片上傳的 API 返回的 header 中可以包含 location: http://api.domain.name/photos/1234

對(duì)于返回?cái)?shù)據(jù),另一個(gè)值得一提的優(yōu)化是使用gzip,這雖然和 API 設(shè)計(jì)本身無(wú)關(guān),只是服務(wù)器配置上的問(wèn)題,之所以特別提出,是因?yàn)?RESTful API 一般都是返回文本數(shù)據(jù),啟用 gzip 通常可以節(jié)省60%-80%以上的帶寬(這個(gè)數(shù)據(jù)很好證明,隨便使用幾個(gè)個(gè) json 文件 gzip下就可以看出來(lái),我測(cè)試幾個(gè) json 文件一般300K左右都能被壓縮成50K左右),尤其是在返回的數(shù)據(jù)比較大情況下,壓縮比更高。不過(guò)啟用gzip 不可避免會(huì)增加 CPU 的負(fù)擔(dān),實(shí)際工程項(xiàng)目中需要權(quán)衡考量。

至于到底用什么用的格式來(lái)返回?cái)?shù)據(jù)?XML?JSON?純文本?但從統(tǒng)計(jì)數(shù)據(jù)來(lái)看 JSON 格式目前是使用做多的 REST API 的輸入輸出格式。

有些系統(tǒng)設(shè)計(jì)采用 application/x-www-form-urlencoded 形式作為輸入內(nèi)容(以key=value&key=value...的形式 POST 去服務(wù)端,其中value使用urlencode)。這樣設(shè)計(jì)優(yōu)點(diǎn)是由于 value 內(nèi)容是純文本,可用自由的定義成各種其它系統(tǒng)方便解析的格式,使得服務(wù)端在解析 model 的上獲取更大的自由度。此外 JAVA 的一個(gè)流行框架 Spring MVC controller 中可以直接使用@RequestParam用一個(gè)函數(shù)參數(shù)自動(dòng)對(duì)應(yīng)上 form post 過(guò)來(lái)的數(shù)據(jù),省去了解析body中 JSON 的麻煩。

個(gè)人更傾向使用 JSON, 因?yàn)楝F(xiàn)在幾乎主流的平臺(tái)和語(yǔ)言都對(duì) JSON 有著穩(wěn)定高效的支持,各種簡(jiǎn)單易用的解析和生成 JOSN 的框架層出不窮,所以建議對(duì)于輸入輸出統(tǒng)一使用 JSON 格式(其中輸入是指 POST, PUT & PATCH API 中放在 http body 中的輸入?yún)?shù))。

<br />

5.根據(jù)不同的 API 操作,設(shè)置合適的 HTTP 狀態(tài)碼和必要的出錯(cuò)信息

根據(jù)不同的 API 操作,設(shè)置合適的 HTTP 狀態(tài)碼和必要的出錯(cuò)信息

使用合理的狀態(tài)碼有助于提高客戶端的易用性,因?yàn)檫@些 HTTP 狀態(tài)代碼本身就有一定的含義,如能在 API 返回信息中合理的利用,可以減少額外的文檔描述,讓API返回結(jié)果“不言自明”。

http status code 的常用應(yīng)用場(chǎng)景如下:

  • 200 OK 用于返回 GET, PUT, PATCH 或 DELETE 的操作。有使用也用來(lái)返回沒(méi)有創(chuàng)建數(shù)據(jù)的 POST 操作;
  • ** 201 Created** 用來(lái)返回 POST 操作并且成功創(chuàng)建了數(shù)據(jù)的情況。新創(chuàng)建的數(shù)據(jù)資源的鏈接應(yīng)該放在location中返回,具體參見(jiàn)這里 ;
  • 204 No Content 用來(lái)返回一次成功的請(qǐng)求,但是該請(qǐng)求返回的 body 為空的情況,如 DELETE 請(qǐng)求;
  • 304 Not Modified 表示緩存沒(méi)有失效,和上次的請(qǐng)求相比,沒(méi)有新的內(nèi)容;
  • 400 Bad Request 用于返回 API 參數(shù)不正確的情況,比如傳入的 JSON 格式錯(cuò)誤無(wú)法解析等;
  • 401 Unauthorized 用于表示請(qǐng)求等 API 缺少身份驗(yàn)證信息;
  • 403 Forbidden 用于表示該資源不允許特定用戶訪問(wèn);
  • 404 Not Found 請(qǐng)求一個(gè)不存在的資源;
  • 429 Too Many Requests 請(qǐng)求過(guò)于頻繁,可以用在客戶端調(diào)用過(guò)于頻繁的情況。

對(duì)于需要提供額外說(shuō)明的錯(cuò)誤類(lèi)型,可以在 HTTP Body 中詳細(xì)描述,便于調(diào)用者排查原因。

{
 "error": {
  "message":"Message describing the error",
  "type":"OAuthException",
  "code":190,
  "error_subcode":460,
  "error_user_title":"A title",
  "error_user_msg":"A message",
  "fbtrace_id":"EJplcsCHuLu" 
  }
}

錯(cuò)誤信息要容易解析,比如上面的錯(cuò)誤信息中,返回的 JSON 數(shù)據(jù)下有個(gè) error 屬性,客戶端只要判斷屬性是否存在即可判斷是否有詳細(xì)的錯(cuò)誤信息。
如果你的API比較復(fù)雜,最好能有文檔按照 error code 分門(mén)別類(lèi)記錄這些 error 產(chǎn)生的原因以及如何應(yīng)對(duì)。

<br />

6.使用 token 機(jī)制設(shè)計(jì)鑒權(quán)和驗(yàn)證系統(tǒng)(Authorization and Authentication[1]

使用 token 機(jī)制設(shè)計(jì)鑒權(quán)和驗(yàn)證系統(tǒng)(Authorization and Authentication

這個(gè)話題可以討論的內(nèi)容有很多,這里主要從實(shí)用的角度來(lái)給出一些解決方案和解決問(wèn)題的思路。
由于 RESTful API 的無(wú)狀態(tài)的特性,所以我們不能依賴(lài)請(qǐng)求前后的上下文來(lái)做鑒權(quán)和用戶驗(yàn)證,那到底該如何區(qū)分調(diào)用者是誰(shuí)從而確定它有沒(méi)有相應(yīng)的權(quán)限調(diào)用某個(gè)API?

我們先來(lái)看個(gè)例子,這個(gè)例子來(lái)自騰訊云微視頻MVS API
你在成功申請(qǐng)騰訊云的微視頻服務(wù)之后會(huì)給你分配 Appid、Secret ID 之類(lèi)的信息??蛻舳嗽谡{(diào)用上傳和刪除視頻之類(lèi)的 API 時(shí), 需要把一個(gè) token 放在 API 請(qǐng)求的 http header 的 Authorization 字段中。其中 token 是按照某種規(guī)則拼接 Appid、Secret ID 生成的。這樣服務(wù)端在收到這個(gè)調(diào)用請(qǐng)求時(shí)就可以區(qū)分這個(gè) API 是哪個(gè)用戶調(diào)用的,該用戶是否有相應(yīng)的權(quán)限(其中 Appid 相當(dāng)于用戶名,Secret ID 相當(dāng)于密碼,應(yīng)當(dāng)妥善保存)。

這種設(shè)計(jì)思想很簡(jiǎn)單,原理就是:針對(duì)特定用戶生成一個(gè) token,之后每次API的調(diào)用請(qǐng)求都帶上這個(gè) token。為了防止 token 泄露引發(fā)的安全問(wèn)題,還應(yīng)該考慮 token 什么時(shí)候失效,什么時(shí)候需要重新生成。說(shuō)到這里,可能會(huì)有人會(huì)問(wèn),為什么不實(shí)施OAuth 2?答案是適用場(chǎng)景不同,部署 OAuth2 也會(huì)將問(wèn)題復(fù)雜化。OAuth 2 適合需要把某一資源暴露給第三方應(yīng)用的情況,比如新浪微博提供 OAuth 2 驗(yàn)證,如果你使用新浪微博登錄豆瓣,在你的同意下(你在微博的登錄界面輸入用戶名密碼,并且確認(rèn)),微博最終會(huì)給豆瓣一個(gè)具有實(shí)效性的 token,豆瓣憑借這個(gè) token 來(lái)讀取你的昵稱(chēng)和頭像信息。想想,如果不使用token,豆瓣只有知道你的用戶名密碼才能讀取昵稱(chēng)和頭像信息,這也是OAuth 2 要解決的一個(gè)問(wèn)題。

在實(shí)際工程實(shí)踐中,常見(jiàn)的場(chǎng)景就是用戶系統(tǒng)。那么到底如何設(shè)計(jì)一個(gè) API 能夠針對(duì)不同的用戶做出鑒權(quán)和驗(yàn)證?結(jié)合 OAuth2,參考上面騰訊云微視頻MVS API的例子,這里給出一個(gè)實(shí)用的解決方案:

  1. 用戶使用戶名密碼或者第三方登錄,最終請(qǐng)求一個(gè)我們?cè)O(shè)計(jì)的登錄 API(這個(gè) API 接受用戶名密碼,或第三方登錄驗(yàn)證結(jié)果);
  2. 服務(wù)端認(rèn)證成功以后,生成一個(gè) token,并將這個(gè) token 和用戶信息關(guān)聯(lián)在一起,同時(shí)返回這個(gè) token 給調(diào)用客戶端;
  3. 客戶端記錄并保存下這個(gè) token;
  4. 下次客戶端發(fā)起和用戶相關(guān)請(qǐng)求 API 都要在 http header 中帶上這個(gè) token;
  5. 服務(wù)端通過(guò)這個(gè) token 去區(qū)分用戶是誰(shuí),判斷這個(gè)用戶是否已經(jīng)登錄和有什么樣的權(quán)限;
  6. 服務(wù)端也要考慮 token 的失效時(shí)間;
  7. 客戶端在發(fā)現(xiàn) token 失效的時(shí)候重新請(qǐng)求新的 token

具體步驟和實(shí)現(xiàn)如下圖:


使用Token驗(yàn)證用戶

細(xì)心的讀者可能要問(wèn):為什么要多一個(gè)步驟使用 token 呢?為什么不直接把用戶名和密碼放在 http header 中直接做授權(quán)和驗(yàn)證?原因是調(diào)用 API 一般會(huì)被頻繁調(diào)用,這樣用戶名和密碼頻繁在網(wǎng)絡(luò)上傳輸,增加了泄漏的危險(xiǎn)。如果使用token,即使泄漏了也不會(huì)暴露用戶的密碼,何況 token 也被經(jīng)常被設(shè)計(jì)成有時(shí)間限制的,超時(shí)以后當(dāng)前 token 就會(huì)失效,需要客戶端重新做驗(yàn)證獲得新的 token,暴露之后的影響很快就會(huì)過(guò)去。

其實(shí)獲取 token,用 token 做授權(quán)和驗(yàn)證和 OAuth 2 如出一轍,手法完全相同的,只是 OAuth 2 有更復(fù)雜的標(biāo)準(zhǔn)步驟去換取這個(gè)token,并且這個(gè) token 的用途不同。OAuth 2 的 token 用來(lái)授權(quán)給第三方使用,我們自己設(shè)計(jì)的系統(tǒng) token 僅限在自己系統(tǒng)本身 API 使用。

<br />

7.如何實(shí)現(xiàn)數(shù)據(jù)的分頁(yè)返回

如何實(shí)現(xiàn)數(shù)據(jù)的分頁(yè)返回

通常情況下一次API調(diào)用不可能返回該資源的所有數(shù)據(jù),因?yàn)?,一?lái)多數(shù)情況下一個(gè)資源包含的數(shù)據(jù)太多,二來(lái)客戶端也沒(méi)有必要一次使用所有數(shù)據(jù),因?yàn)橛脩舳虝r(shí)間上根本就看不了那么多內(nèi)容,完全可以在需要的時(shí)候加載更多。

RESTful API 一般有兩種形式的設(shè)計(jì),一種是使用類(lèi)似 Facebook Graph API 的方法,它把分頁(yè)信息和數(shù)據(jù)一起返回,調(diào)用者只需要再次請(qǐng)求 next 中 URL 就可以獲取下一頁(yè)的數(shù)據(jù),這種方式優(yōu)點(diǎn)是靈活和直觀,可以隨意添加和分頁(yè)相關(guān)的其他屬性,例如總記錄數(shù),總頁(yè)數(shù)等等。其中cursors用來(lái)解決“流”的問(wèn)題(由于數(shù)據(jù)是動(dòng)態(tài)增加的,基于舊數(shù)據(jù)的頁(yè)數(shù)和頁(yè)碼會(huì)失效,于是引入 cursors 來(lái)標(biāo)記數(shù)據(jù)位置,關(guān)于這個(gè)問(wèn)題,Twitter 在介紹其timeline API時(shí)有圖文并貌的詳細(xì)描述)。

// Facebook Graph API paging 
{
  "data":[{...},{...}],
  "paging": {
    "cursors":{ "after":"MTAxNTExOTQ1MjAwNzI5NDE=", "before":"NDMyNzQyODI3OTQw" }, 
    "previous":"https://graph.facebook.com/me/albums?limit=25&before=NDMyNzQyODI3OTQw", 
    "next":"https://graph.facebook.com/me/albums?limit=25&after=MTAxNTExOTQ1MjAwNzI5NDE="
  }
}

另一種符合WEB標(biāo)準(zhǔn)的做法是使用 link header,簡(jiǎn)單來(lái)說(shuō)就是在 http header 使用 link字段,提供一個(gè)和超鏈接一樣目的 URL 地址,來(lái)實(shí)現(xiàn)不同資源之間的轉(zhuǎn)跳。如GitHub的Api文檔是這樣規(guī)定分頁(yè)信息的

Link: <https://api.github.com/user/repos?page=3&per_page=100>; rel="next",
  <https://api.github.com/user/repos?page=50&per_page=100>; rel="last"

這種做法缺點(diǎn)是不太直觀,如果使用 Postman 之類(lèi)工具調(diào)試的時(shí)候,需要手動(dòng)找到header中的內(nèi)容復(fù)制出來(lái)才能發(fā)出下一頁(yè)的請(qǐng)求。而第一種實(shí)現(xiàn),直接點(diǎn)擊這個(gè)鏈接即可。但第二種實(shí)現(xiàn)的優(yōu)點(diǎn)是不會(huì)干擾數(shù)據(jù),返回內(nèi)容都是數(shù)據(jù)本身,無(wú)需在數(shù)據(jù)上嵌入額外的屬性來(lái)說(shuō)明分頁(yè)信息,簡(jiǎn)單干凈。至于如何選擇,完全要看個(gè)人偏好和具體使用場(chǎng)景了。

<br />

8.如何處理有關(guān)聯(lián)資源的返回?cái)?shù)據(jù)

如何處理返回?cái)?shù)據(jù)中的關(guān)聯(lián)資源

考慮這么一個(gè)情況:有一個(gè) API,輸入一個(gè)指定用戶 id,返回一個(gè)該用戶所有評(píng)論信息。最終要在 UI 上顯示的,除了該用戶評(píng)論的具體文本內(nèi)容以外,還有用戶名,頭像,個(gè)人簡(jiǎn)介之類(lèi)和該用戶相關(guān)的詳細(xì)信息。該API的返回值應(yīng)該如何設(shè)計(jì)?

對(duì)客戶端來(lái)說(shuō),最直觀和容易處理的返回形式如下:

{
  data: [
    {user_id: "1234", avatar: "a.jpg", nick_name:"Jeffrey", comment:"RESTful Service API"}, 
    {user_id: "1234", avatar: "a.jpg", nick_name:"Jeffrey", comment:"J:"}
    ...
  ]
}

你肯定一眼就能看出問(wèn)題,是的,返回?cái)?shù)據(jù)中 avatar 和 name 是每條數(shù)據(jù)都是重復(fù)的,所以你也可以這樣設(shè)計(jì)返回?cái)?shù)據(jù):
先返回該用戶的所有評(píng)論 /comments?user=1234

{
  data: [
    {user_id: "1234", comment:"RESTful Service API"}, 
    {user_id: "1234", comment:"J:"},
    ...
  ]
}

再通過(guò)請(qǐng)求該用戶 API 的相關(guān)內(nèi)容 /users/1234:
{user_id: "1234", avatar: "a.jpg", nickName:"Jeffrey"...}

這種情況下其實(shí)可以將依賴(lài)資源嵌入返回對(duì)象中,避免了客戶端需要再一次發(fā)起請(qǐng)求來(lái)獲取這個(gè) user 的詳細(xì)信息:
/comments?user=1234 直接返回類(lèi)似這樣的信息即可:

{
  data: [
   {comment:"RESTful Service API"},
   {comment:"J:"},
   ...
 ],

 comment_user: {user_id: "1234", avatar: "a.jpg", nickName:"Jeffrey"...}
}

<br />

9.考慮啟用 HTTP 緩存機(jī)制

考慮啟用 HTTP 緩存機(jī)制

HTTP協(xié)議本身支持兩種緩存機(jī)制: ETagLast-Modified。由于這部分內(nèi)容更多屬于服務(wù)器配置范疇,這里只做簡(jiǎn)單介紹:

  1. ETag:HTTP 請(qǐng)求中在 header 中包含一個(gè)內(nèi)容的 hash,如果返回結(jié)果沒(méi)有變化,該請(qǐng)求會(huì)直接返回304 Not Modified,而不是所有數(shù)據(jù)內(nèi)容本身
  2. Last-Modified: 和 Etag 工作原理差不多,只是使用時(shí)間戳作為內(nèi)容是否過(guò)期的標(biāo)志。

需要自己配制 WEB Server 的同學(xué)可以自行搜尋相關(guān)內(nèi)容,如果你使用Nginx,可以參考這里: A Guide to Caching with NGINX and NGINX Plus。

<br />

10.限制 API 調(diào)用頻次(Rate limiting)

限制 API 調(diào)用頻次

出于防止惡意訪問(wèn)和服務(wù)器性能壓力考慮,限制 API 訪問(wèn)頻次是非常有必要的,尤其對(duì)于大型系統(tǒng)而言。如果一個(gè)客戶端請(qǐng)求 API 的頻率太快,根據(jù)HTTP協(xié)議,可以返回429 Too Many Requests。

如果要為客戶端提供更加詳細(xì)的調(diào)用頻次和訪問(wèn)次數(shù)之類(lèi)的信息,除了提供文檔說(shuō)明以外,還可以在 http header 用自定義字段的形式提供,比如 Twitter API 是這樣做的:
X-Rate-Limit-Limit: 該請(qǐng)求的調(diào)用上限
X-Rate-Limit-Remaining: 15分鐘內(nèi)還可以調(diào)用多少次
X-Rate-Limit-Reset: 還有多少秒之后訪問(wèn)限制會(huì)被重置

我們可以根據(jù)具體需求在 http header 中使用類(lèi)似的形式,提供對(duì)API調(diào)用頻率和訪問(wèn)限制的相關(guān)信息。當(dāng)然,文檔記錄也是一個(gè)不錯(cuò)的選擇,前提是你能保持文檔和代碼同步更新。

<br />

11.盡可能的使用 HTTPS,涉及用戶驗(yàn)證的 API 一定要強(qiáng)制啟用 HTTPS

盡可能的使用 HTTPS,涉及用戶驗(yàn)證的 API 一定要強(qiáng)制啟用 HTTPS

在閱讀第六章“使用 token 機(jī)制設(shè)計(jì)鑒權(quán)和驗(yàn)證”時(shí),可能已經(jīng)有讀者感到 RESTful API 如果通過(guò) HTTP 明文傳遞會(huì)有很大的安全問(wèn)題。如果用于鑒權(quán)的 app id 和 Secret,甚至是用戶名密碼通過(guò)明文傳遞,那么它們很容易被截獲和保存,完全沒(méi)有安全性可言。

所以凡是涉及任何和用戶特定信息相關(guān)內(nèi)容 API 都要通過(guò) HTTPS 暴露給調(diào)用者。事實(shí)上,你的 AP I應(yīng)該全部使用 HTTPS。HTTPS 現(xiàn)在已經(jīng)是各種網(wǎng)絡(luò)服務(wù)的標(biāo)配(比如 Xcode 默認(rèn)不允許請(qǐng)求不安全的 HTTP 信息)

順便提下,如果你的WEB Server 是 Nginx,在部署了 HTTPS 的情況下,下面兩個(gè)選項(xiàng)務(wù)必仔細(xì)設(shè)置,因?yàn)檫@個(gè)兩個(gè)簡(jiǎn)單的設(shè)置可以很大程度上避免一些安全問(wèn)題:

  1. ssl_prefer_server_ciphers: 表示服務(wù)端加密算法優(yōu)先于客戶端加密算法,主要是防止降級(jí)攻擊 (downgrade attack)。
  2. Strict-Transport-Security(HSTS):告訴瀏覽器這個(gè)域名在指定的時(shí)間(max-age)內(nèi)應(yīng)該強(qiáng)制使用 HTTPS 訪問(wèn)。

<br />

小結(jié)

以上內(nèi)容,是對(duì)工作中遇到的一些關(guān)于 RESTful API 設(shè)計(jì)問(wèn)題的總結(jié)和實(shí)踐。其中也總結(jié)和歸納很多現(xiàn)有系統(tǒng)的 API 設(shè)計(jì)原則和大量前人工作成果。這里主要做了一個(gè)編輯和整理(也會(huì)繼續(xù)整理和更新相關(guān)內(nèi)容),希望能給一起前進(jìn)的人提供一個(gè)參考,設(shè)計(jì)優(yōu)秀系統(tǒng) API,造福廣大程序員用戶。

參考文檔


  1. Authentication: 驗(yàn)證你是誰(shuí),比如你輸入用戶名和密碼的過(guò)程就是Authentication。Authorization:驗(yàn)證你是否有相應(yīng)的權(quán)限的過(guò)程,比如確定你是否有權(quán)限訪問(wèn)某個(gè)文件的過(guò)程就是Authorization。 ? ?

  2. 為了行文簡(jiǎn)潔,此類(lèi)API都省略前面的域名和前綴部分比如 /getAllUser 實(shí)際上應(yīng)該是類(lèi)似 https://api.thedomain.com/v1/getAllUser,下同。 ?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容