
使用NestJS和Prisma構建REST API:身份驗證
圖8-1 REST成熟度模型
在第0級中,Web服務只是使用HTTP作為傳輸方式,實際上只是遠程方法調用(RPC)的一種具體形式。SOAP和XML-RPC都屬于此級別。比如,在一個醫院掛號系統中,醫院會通過某個URI來暴露出該掛號服務端點(Service Endpoint)。然后患者會向該URI發送一個文檔作為請求,文檔中包含了請求的所有細節。
POST /appointmentService HTTP/1.1
[省略了其他頭的信息……]
<openSlotRequest date = "2010-01-04" doctor = "mjones"/>
然后服務器會傳回一個包含了所需信息的文檔。
HTTP/1.1 200 OK
[省略了其他頭的信息……]
<openSlotList>
<slot start = "1400" end = "1450">
<doctor id = "mjones"/>
</slot>
<slot start = "1600" end = "1650">
<doctor id = "mjones"/>
</slot>
</openSlotList>
在這個例子中我們使用了XML,但是內容實際上可以是任何格式,比如JSON、YAML、鍵值對等,或者其他自定義的格式。
有了這些信息,下一步就是創建一個預約。這同樣可以通過向某個端點發送一個文檔來完成。
POST /appointmentService HTTP/1.1
[省略了其他頭的信息……]
<appointmentRequest>
<slot doctor = "mjones" start = "1400" end = "1450"/>
<patient id = "jsmith"/>
</appointmentRequest>
如果一切正常,那開發者能夠收到一個預約成功的響應。
HTTP/1.1 200 OK
[省略了其他頭的信息……]<appointment>
<slot doctor = "mjones" start = "1400" end = "1450"/>
<patient id = "jsmith"/>
</appointment>
如果發生了問題,比如有人在我前面預約上了,那我會在響應體中收到某條錯誤信息。
HTTP/1.1 200 OK
[省略了其他頭的信息……]
<appointmentRequestFailure>
<slot doctor = "mjones" start = "1400" end = "1450"/>
<patient id = "jsmith"/>
<reason>Slot not available</reason>
</appointmentRequestFailure>
到目前為止,這都是非常直觀的基于RPC風格的系統。它是簡單的,因為只有Plain Old XML(POX)在這個過程中被傳輸。如果你使用SOAP或者XML-RPC,原理也是基本相同的,唯一的不同是——你將XML消息包含在了某種特定的格式中。
在第1級中,Web服務引入了“資源”的概念,每個資源有對應的標識符和表達。所以,不是將所有的請求發送到單個服務端點,而是和單獨的資源進行交互。
因此在我們的首個請求中,對指定醫生會有一個對應的資源。
POST /doctors/mjones HTTP/1.1
[省略了其他頭的信息……]
<openSlotRequest date = "2010-01-04"/>
響應會包含一些基本信息,包含了各個時間段的就診時間信息,這些就診時間可以被作為資源單獨處理。
HTTP/1.1 200 OK[省略了其他頭的信息……]
<openSlotList>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/>
<slot id = "5678" doctor = "mjones" start = "1600" end = "1650"/>
</openSlotList>
有了這些資源后,創建一個預約就是向某個特定的就診時間發送請求。
POST /slots/1234 HTTP/1.1
[省略了其他頭的信息……]
<appointmentRequest>
<patient id = "jsmith"/>
</appointmentRequest>
如果一切順利,則會收到和前面類似的響應:
HTTP/1.1 200 OK
[省略了其他頭的信息……]
<appointment>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/>
<patient id = "jsmith"/>
</appointment>
在第2級中,Web服務使用不同的HTTP方法來進行不同的操作,并且使用HTTP狀態碼來表示不同的結果。例如,GET方法用來獲取資源,DELETE方法用來刪除資源。
在醫院掛號系統中,獲取醫生的就診時間信息需要使用GET。
GET /doctors/mjones/slots?date=20100104&status=open HTTP/1.1
Host: royalhope.nhs.uk
響應和之前使用POST發送請求時一致。
HTTP/1.1 200 OK
[省略了其他頭的信息……]
<openSlotList><slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/>
<slot id = "5678" doctor = "mjones" start = "1600" end = "1650"/>
</openSlotList>
像上面那樣使用GET來發送一個請求是至關重要的。HTTP將GET定義為一個安全的操作,它并不會對任何事物的狀態造成影響。這也就允許我們可以用不同的順序若干次調用GET請求而每次還能夠獲取到相同的結果。一個重要的結論就是,GET允許參與到路由中的參與者使用緩存機制,該機制是讓目前的Web運轉良好的關鍵因素之一。HTTP包含許多方法來支持緩存,這些方法可以在通信過程中被所有的參與者使用。通過遵守HTTP的規則,我們可以很好地利用該緩存。
為了創建一個預約,我們需要使用一個能夠改變狀態的請求方式。
這里使用和前面相同的一個POST請求。
POST /slots/1234 HTTP/1.1
[省略了其他頭的信息……]
<appointmentRequest>
<patient id = "jsmith"/>
</appointmentRequest>
如果一切順利,則服務會返回一個201響應來表明新增了一個資源。這是與第1級的POST響應完全不同的。第2級中的操作響應都有統一的返回狀態碼。
HTTP/1.1 201 Created
Location: slots/1234/appointment
[省略了其他頭的信息……]
<appointment>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/>
<patient id = "jsmith"/>
</appointment>
201響應包含了一個Location屬性,它是一個URI。將來客戶端可以通過GET請求獲得該資源的狀態。以上的響應還包含該資源的信息,從而省去了一個獲取該資源的請求。當出現問題時,第2級和第1級還有一個不同之處。比如某人預約了該時段:
HTTP/1.1 409 Conflict
[various headers]
<openSlotList>
<slot id = "5678" doctor = "mjones" start = "1600" end = "1650"/>
</openSlotList>
在上例中,409表明該資源已經被更新了。與使用200作為響應碼再附帶一個錯誤信息相比,在第2級中我們會明確響應碼的含義,以及其所對應的響應信息。
在第3級中,Web服務使用HATEOAS。HATEOAS是Hypertext AsThe Engine Of Application State的縮寫,是指在資源的表達中包含了鏈接信息,客戶端可以根據鏈接來發現可以執行的動作。
從上述REST成熟度模型中可以看到,使用HATEOAS的REST服務是成熟度最高的,也是Roy Fielding所推薦的“超文本驅動”的做法。對于不使用HATEOAS的REST服務,客戶端和服務器的實現之間是緊密耦合的。客戶端需要根據服務器提供的相關文檔來了解所暴露的資源和對應的操作。當服務器發生變化(如修改了資源的URI)時,客戶端也需要進行相應的修改。而在使用HATEOAS的REST服務中,客戶端可以通過服務器提供的資源的表達來智能地發現可以執行的操作。當服務器發生了變化時,客戶端并不需要做出修改,因為資源的URI和其他信息都是被動態發現的。下面是一個HATEOAS的例子。
{
"id": 711,
"manufacturer": "bmw",
"model": "X5",
"seats": 5,"drivers": [
{
"id": "23",
"name": "Way Lau",
"links": [
{
"rel": "self",
"href": "/api/v1/drivers/23"
}
]
}
]
}
回到我們的醫院掛號系統案例中,還是使用在第2級中使用過的GET作為首個請求。
GET /doctors/mjones/slots?date=20100104&status=open HTTP/1.1
Host: royalhope.nhs.uk
但是響應中添加了一個新元素。
HTTP/1.1 200 OK
[省略了其他頭的信息……]
<openSlotList>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450">
<link rel = "/linkrels/slot/book"
uri = "/slots/1234"/>
</slot>
<slot id = "5678" doctor = "mjones" start = "1600" end = "1650">
<link rel = "/linkrels/slot/book"
uri = "/slots/5678"/>
</slot>
</openSlotList>
每個就診時間信息現在都包含一個URI,用來告訴我們如何創建一個預約。
超媒體控制(Hypermedia Control)的關鍵在于:它告訴我們下一步能夠做什么,以及相應資源的URI。比如,我們事先就可以知道去哪個地址發送預約請求,因為響應中的超媒體控制直接在響應體中告訴了我們該如何做。
預約的POST請求與第2級中類似。
POST /slots/1234 HTTP/1.1
[省略了其他頭的信息……]
<appointmentRequest>
<patient id = "jsmith"/>
</appointmentRequest>
響應包含了一系列的超媒體控制,用來告訴我們后面可以進行什么操作。
HTTP/1.1 201 Created
Location: http://royalhope.nhs.uk/slots/1234/appointment
[省略了其他頭的信息……]
<appointment>
<slot id = "1234" doctor = "mjones" start = "1400" end = "1450"/>
<patient id = "jsmith"/>
<link rel = "/linkrels/appointment/cancel"
uri = "/slots/1234/appointment"/>
<link rel = "/linkrels/appointment/addTest"
uri = "/slots/1234/appointment/tests"/>
<link rel = "self"
uri = "/slots/1234/appointment"/>
<link rel = "/linkrels/appointment/changeTime"
uri = "/doctors/mjones/slots?date=20100104@status=open"/>
<link rel = "/linkrels/appointment/updateContactInfo"
uri = "/patients/jsmith/contactInfo"/>
<link rel = "/linkrels/help"
uri = "/help/appointment"/>
</appointment>
超媒體控制的一個顯著優點是:它能夠在保證客戶端不受影響的條件下,改變服務器返回的URI方案。只要客戶端查詢“addTest”這個URI,后臺開發團隊就可以根據需要隨意修改與之對應的URI(只有最初的入口URI不能被修改)。
超媒體控制的另一個顯著優點是:它能夠幫助客戶端開發人員進行探索。其中的鏈接告訴了客戶端開發人員下面可能需要執行的操作。它并不會告訴所有的信息,但是至少它提供了一個思考的起點,引導開發人員在協議文檔中查看相應的URI。
同樣地,它也讓服務器端的團隊可以通過向響應中添加新的鏈接來增加功能。比如,如果客戶端開發人員發現了一個之前未知的鏈接,那他們就會知道這個鏈接是服務器端提供的新的功能。
下面介紹幾種簡潔的REST API設計的最佳實踐,可以作為真假REST的一個判別依據。
使用名詞來定義接口。
/resources
/resources/1024
不應該使用動詞:
/getAllResources
/createNewResource
/deleteAllResources
如果要改變資源的狀態,要使用PUT、POST和DELETE。下面使用GET方法來修改user的狀態是錯誤的:
GET /users/711?activate
或
GET /users/711/activate
不要混淆名詞的單復數。保持簡單,只用復數名詞來定義所有資源。
/cars 代替 /car
/users 代替 /user
/products 代替 /product/settings 代替 /setting
GET /cars/711/drivers/ 返回 711 號 car 的所有 driver 列表
GET /cars/711/drivers/4 返回 711 號 car 的 4 號 driver
客戶端、服務器都需要知道相互之間的通信格式。這些格式可以定義在HTTP header里面。
·Content-Type定義了請求格式。
·Accept定義了接收相應的格式列表。
HATEOAS是REST架構風格中最復雜的約束,也是構建成熟REST服務的核心。它的重要性在于打破了客戶端和服務器之間嚴格的合約,使得客戶端可以更加智能和自適應,而REST服務本身的演化和更新也變得更加容易。
下面是一個HATEOAS的例子。
{
"id": 711,
"manufacturer": "bmw",
"model": "X5",
"seats": 5,
"drivers": [
{
"id": "23",
"name": "Stefan Jauker",
"links": [
{
"rel": "self",
"href": "/api/v1/drivers/23"
}
]
}
]}
過濾:
GET /cars?color=red
GET /cars?seats<=2
排序:
GET /cars?sort=-manufactorer,+model
字段選擇:
GET /cars?fields=manufacturer,model,id,color
分頁:
GET /cars?offset=10&limit=5
版本號使用簡單的序號,并避免點號,如2.5等。正確用法如下。
/blog/api/v1
HTTP狀態碼(HTTP Status Code)是用來表示網頁服務器HTTP響應狀態的3位數字代碼。它由RFC 2616規范定義,并得到RFC 2518、RFC 2817、RFC 2295、RFC 2774、RFC 4918等規范的擴展。
在設計API處理錯誤時,應該充分使用HTTP狀態碼,而不是簡單地拋出一個“500-Internal Server Error(內部服務器錯誤)”。所有的異常都應該有一個錯誤的payload作為映射,下面是一個例子。
{
"errors": [
{
"userMessage": "Sorry, the requested resource does not exist",
"internalMessage": "No car found in the database",
"code": 34,
"more info": "http://dev.mwaysolutions.com/blog/api/v1/errors/12345"
}
]
}
文章轉自微信公眾號@IT大咖說