2020年9月10日

使用 TypeScript、PostgreSQL 和 Prisma 構建後端:身份驗證與授權

在本系列的第三部分中,我們將探討如何使用 Prisma 進行令牌儲存來實現無密碼身份驗證,並實施授權,從而保護 REST API 的安全。

Backend with TypeScript, PostgreSQL & Prisma: Authentication & Authz

引言

本系列的目標是透過解決一個具體問題來探索和展示現代後端開發中的不同模式、問題和架構:一個線上課程的評分系統。這是一個很好的例子,因為它具有多樣化的關係型別,並且足夠複雜,足以代表一個真實世界的用例。

直播錄影已在上方提供,其內容與本文相同。

本系列將涵蓋的內容

本系列將重點關注資料庫在後端開發各個方面的作用,包括

主題部分資料建模第 1 部分CRUD第 1 部分聚合第 1 部分REST API 層第 2 部分驗證第 2 部分測試第 2 部分無密碼身份驗證第 3 部分(當前)授權第 3 部分(當前)與外部 API 整合第 3 部分(當前)部署即將推出

您今天將學到什麼

在第一篇文章中,您為問題域設計了一個資料模型,並編寫了一個使用Prisma Client將資料儲存到資料庫的種子指令碼。

在本系列的第二篇文章中,您在第一篇文章的資料模型和Prisma schema之上構建了一個REST API。您使用Hapi構建了 REST API,它允許透過 HTTP 請求對資源執行 CRUD 操作。

在本系列的第三篇文章中,您將瞭解身份驗證和授權背後的概念、兩者之間的區別,以及如何使用JSON Web Tokens (JWT)透過 Hapi 實現基於電子郵件的無密碼身份驗證和授權,以保護 REST API 的安全。

具體來說,您將開發以下方面

  1. 無密碼身份驗證:增加透過傳送包含唯一令牌的電子郵件來登入和註冊的功能。使用者透過將收到的電子郵件令牌傳送到 API 並獲得一個長期有效的 JWT 令牌來完成身份驗證過程,從而獲得訪問需要身份驗證的 API 端點的許可權。
  2. 授權:新增授權邏輯以限制使用者可以訪問和操作的資源。

到本文結束時,REST API 將透過身份驗證進行保護,以訪問 REST 端點。此外,您將使用 Hapi 的pre路由選項,為部分端點新增授權規則,從而根據特定使用者的許可權授予訪問許可權。包含所有端點授權規則的 API 將在此GitHub 倉庫中提供。

注意:在整個指南中,您會發現各種檢查點,它們使您能夠驗證是否正確執行了步驟。

先決條件

假定知識

本系列假定您具備 TypeScript、Node.js 和關係資料庫的基礎知識。如果您有 JavaScript 經驗但尚未嘗試過 TypeScript,您仍然可以繼續學習。本系列將使用 PostgreSQL。但是,大多數概念也適用於其他關係資料庫,例如 MySQL。熟悉 REST 概念會有所幫助。除此之外,不需要任何 Prisma 先驗知識,因為本系列會涵蓋這些內容。

開發環境

您應該安裝以下軟體

  • Node.js(版本 10、12 或 14)
  • Docker(將用於執行開發 PostgreSQL 資料庫)

如果您正在使用 Visual Studio Code,建議安裝Prisma 擴充套件,用於語法高亮、格式化和其他輔助功能。

注意:如果您不想使用 Docker,可以設定一個本地 PostgreSQL 資料庫在 Heroku 上託管的 PostgreSQL 資料庫

外部服務

需要一個SendGrid賬戶,以便您可以從後端傳送無密碼身份驗證電子郵件。SendGrid 提供免費套餐,每天最多可傳送 100 封電子郵件。

註冊後,前往 SendGrid 控制檯的API Keys,生成一個 API 金鑰,並妥善保管。

克隆倉庫

本系列的原始碼可在GitHub上找到。

要開始,請克隆倉庫並安裝依賴項

注意:透過檢出part-3分支,您將能夠從相同的起點開始閱讀本文。

啟動 PostgreSQL

要啟動 PostgreSQL,請從real-world-grading-app資料夾中執行以下命令

注意: Docker 將使用docker-compose.yml檔案啟動 PostgreSQL 容器。

身份驗證與授權概念

在深入實施之前,我們將回顧一些與身份驗證和授權相關的概念。

雖然這兩個術語經常互換使用,但身份驗證和授權服務於不同的目的。一般來說,它們都以互補的方式用於保護應用程式。

簡單來說,身份驗證是驗證使用者是誰的過程,而授權是驗證使用者擁有什麼訪問許可權的過程。

現實世界中身份驗證的一個例子是有效的護照。您長得像官方檔案(難以偽造)上的人這一事實,證明了您就是您所聲稱的身份。例如,當您去機場時,您出示護照,然後被允許透過安檢。

在同一個例子中,授權是您被允許登機的過程:您出示登機牌(通常會掃描並與航班乘客資料庫進行核對),地勤人員會授權您登機。

Web 應用程式中的身份驗證

Web 應用程式通常使用使用者名稱和密碼來驗證使用者身份。如果提供了有效的使用者名稱和密碼,應用程式就可以驗證您是您所聲稱的使用者,因為密碼應該只有您和應用程式知道。

注意:使用使用者名稱/密碼身份驗證的 Web 應用程式很少將密碼明文儲存在資料庫中。相反,它們使用一種稱為雜湊的技術來儲存密碼的雜湊值。這允許後端在不知道密碼的情況下驗證密碼。

雜湊函式是一種數學函式,它接受任意輸入,並且在給定相同輸入的情況下,總是生成相同的固定長度字串/數字。雜湊函式的強大之處在於,您可以從密碼生成雜湊值,但不能從雜湊值反向推匯出密碼。

這允許在不儲存實際密碼的情況下驗證使用者提交的密碼。儲存密碼雜湊值可以在資料庫訪問被洩露的情況下保護使用者,因為無法使用雜湊密碼登入。

近年來,鑑於許多重要網站遭到洩露,網路安全已成為一個日益嚴重的問題。這一趨勢影響了安全方法的採取,引入了更安全的身份驗證方法,例如多因素身份驗證

多因素身份驗證是一種身份驗證方法,使用者在成功提供兩項或更多證據(也稱為因素)後才能透過身份驗證。例如,從 ATM 取款時,需要兩個身份驗證因素:擁有銀行卡和 PIN 碼。

由於網頁應用程式難以驗證卡片所有權,因此多因素身份驗證通常透過使用身份驗證器應用程式(安裝在智慧手機或專用裝置上生成這些密碼的應用程式)生成的一次性令牌來補充使用者名稱/密碼。

在本文中,您將實現基於電子郵件的無密碼身份驗證——一種兩步式方法,可改善使用者體驗和安全性。其工作原理是在嘗試登入時向用戶的電子郵件帳戶傳送一個秘密令牌。一旦使用者開啟電子郵件並將令牌傳遞給應用程式,應用程式就可以驗證使用者身份並確定使用者是該電子郵件帳戶的所有者。

這種方法依賴於使用者的電子郵件服務,可以假定該服務已經驗證了使用者的身份。使用者體驗得到改善,因為使用者無需設定和記住密碼。安全性也得到了增強,因為應用程式擺脫了密碼管理職責,而這可能成為攻擊面。

將身份驗證外包給使用者的電子郵件賬戶意味著應用程式將繼承使用者電子郵件賬戶安全的優點和缺點。但如今,大多數電子郵件服務都提供二次身份驗證和其他安全措施的選項。

儘管如此,這種方法避免了使用者選擇弱密碼,並很可能在多個網站上重複使用它們。完全取消密碼意味著這些使用者更安全。不再存在可以被猜測、暴力破解或完全破解的密碼。

身份驗證和註冊/登入流程

基於電子郵件的無密碼身份驗證是一個兩步過程,涉及兩種令牌型別。

身份驗證流程如下

  1. 使用者透過有效載荷中的電子郵件呼叫 API 中的/login端點,以開始身份驗證過程。
  2. 如果電子郵件是新的,則在User表中建立使用者。
  3. 後端生成一個電子郵件令牌並儲存到Token表中
  4. 電子郵件令牌已傳送至使用者的郵箱
  5. 使用者將電子郵件令牌(透過電子郵件接收)和電子郵件地址傳送到/authenticate端點
  6. 後端驗證使用者傳送的電子郵件令牌。如果有效且令牌未過期,則生成一個 JWT 令牌並儲存到Token表中。
  7. JWT 令牌透過Authorization頭髮送回使用者。

有兩種令牌型別

  1. 電子郵件令牌:一個八位數字令牌,有效期短,例如 10 分鐘,併發送到使用者的電子郵件。此令牌的唯一目的是驗證使用者與該電子郵件關聯,這意味著它不授予對任何評分應用程式相關端點的訪問許可權。
  2. 身份驗證令牌:一個在其有效載荷中包含tokenId的 JWT 令牌。此令牌可在向 API 發出請求時透過在Authorization頭中傳遞它來訪問受保護的端點。此令牌是長期有效的,因為它有效期為 12 小時。

透過這種身份驗證策略,一個端點即可處理登入和註冊。這之所以可能,是因為登入和註冊之間唯一的區別在於您是否在“User”表中建立了一行(如果使用者已存在)。

JSON Web 令牌

JSON Web 令牌 (JWT) 是一種在兩個方之間安全地表示宣告的開放標準方法。該標準定義了一種緊湊、自包含的方式,用於將資訊以 JSON 物件的形式在各方之間安全傳輸。這些資訊可以被驗證和信任,因為它們經過數字簽名。

一個 JWT 令牌包含三個用 Base64 編碼的部分:header(頭部)payload(載荷)signature(簽名),其格式如下(各部分用.分隔)

注意:Base64 是另一種表示資料的方式。它不涉及任何加密

如果您使用 Base64 解碼上述頭部和載荷,您將得到以下內容

  • 頭部:{"alg":"HS256","typ":"JWT"}
  • 載荷:{"tokenId":9}

令牌的簽名部分是透過將頭部載荷金鑰透過簽名演算法(本例中為 HS256)生成。金鑰僅為後端所知,用於驗證令牌的真實性。

在本文中,JWT 將用於長期有效的身份驗證令牌。令牌的載荷將包含tokenId,該 ID 將儲存在資料庫中,並引用建立該令牌的使用者。這允許後端找到關聯的使用者。

注意:這種方法稱為有狀態 JWT,其中令牌引用儲存在資料庫中的會話。雖然這意味著驗證請求需要往返資料庫,這會增加處理請求所需的時間,但這種方法更安全,因為令牌可以由後端撤銷。

向 Prisma schema 新增令牌模型

您需要將令牌儲存在資料庫中,以便在發出請求時進行驗證。在此步驟中,您將向 Prisma schema 新增一個新的Token模型,並更新User模型,使某些欄位變為可選。

開啟位於prisma/schema.prisma的 Prisma schema,並按如下方式更新

我們來回顧一下所引入的更改

  • 啟用connectOrCreatetransactionApi預覽功能。這些功能將在後續步驟中使用。
  • 移除aggregateApi預覽功能,該功能自Prisma 2.5.0起已穩定。
  • User模型中,firstNamelastName現在是可選的。這允許使用者僅使用電子郵件登入/註冊。
  • 添加了一個新的Token模型。每個使用者可以擁有多個令牌,形成 1-n 關係。Token模型包含相關欄位,用於處理過期時間、兩種令牌型別(使用TokenType列舉)以及電子郵件令牌的儲存。

要遷移資料庫 schema,請按如下方式建立並執行遷移

檢查點:您應該在輸出中看到類似以下內容

注意:執行prisma migrate dev命令也會預設生成 Prisma Client。

新增電子郵件傳送功能

由於後端將在使用者登入時傳送電子郵件,您將建立一個外掛,將電子郵件傳送功能暴露給應用程式的其餘部分。Hapi 外掛將遵循與 Prisma 外掛類似的約定。

本文將使用 SendGrid 和@sendgrid/mail npm 包,以便與 SendGrid API 輕鬆整合。

新增依賴項

建立電子郵件外掛

src/plugins/資料夾中建立一個名為email.ts的新檔案

並將以下內容新增到檔案中

該外掛將在server.app物件上暴露sendEmailToken函式,該函式可在您的路由處理程式中訪問。它將使用SENDGRID_API_KEY環境變數,您將在生產環境中使用 SendGrid 控制檯中的金鑰設定該變數。在開發過程中,您可以將其留空,令牌將被記錄下來,而不是透過電子郵件傳送。

最後,在server.ts中註冊外掛

使用 Hapi 新增身份驗證

為了實現身份驗證,您將首先定義/login/register路由,它們將處理在資料庫中建立使用者和令牌、傳送電子郵件令牌、驗證電子郵件以及生成 JWT 身份驗證令牌。值得注意的是,這兩個端點將處理身份驗證過程,但它們不會保護 API。

為了保護 API 的安全,一旦定義了這兩個路由,您將定義一個身份驗證策略,該策略使用hapi-auth-jwt2庫提供的jwt方案。

注意: Hapi 中的身份驗證基於方案(schemes)策略(strategies)的概念。方案是處理身份驗證的一種方式,而策略是方案的預配置例項。在本文中,您只需要定義基於jwt身份驗證方案的策略。

您將把所有這些邏輯封裝在一個auth外掛中。

新增依賴項

首先,將以下依賴項新增到您的專案中

建立身份驗證外掛

接下來,您將建立一個身份驗證外掛來封裝身份驗證邏輯。

src/plugins/資料夾中建立一個名為auth.ts的新檔案

並將以下內容新增到檔案中

注意: 身份驗證外掛定義了對prismahapi-auth-jwt2app/email外掛的依賴。prisma 外掛在本系列的第 2 部分中定義,將用於訪問 Prisma Client。hapi-auth-jwt2外掛定義了jwt身份驗證方案,您將使用它來定義身份驗證策略。最後,app/email將確保您可以訪問sendEmailToken函式。

定義登入端點

authPluginregister函式中,定義一個新的登入路由,如下所示

注意: options.auth設定為 false,以便在您設定預設身份驗證策略後,該端點將保持開放,預設情況下,所有未明確停用身份驗證的路由都需要身份驗證。

在外掛的 register 函式外部,新增以下內容

loginHandler執行以下操作

  • 電子郵件取自請求有效載荷
  • 生成一個令牌,然後儲存到資料庫
  • 使用connectOrCreate,如果有效載荷中的電子郵件地址使用者不存在,則建立該使用者。否則,將建立與現有使用者的關係。
  • 令牌會發送到有效載荷中的電子郵件地址(如果未設定SENDGRID_API_KEY,則會記錄到控制檯)

最後,在server.ts中註冊外掛

檢查點

  1. 使用npm run dev啟動伺服器
  2. 使用 curl 對/login端點進行 POST 呼叫:curl --header "Content-Type: application/json" --request POST --data '{"email":"test@test.io"}' localhost:3000/login。您應該看到後端記錄的令牌:email token for test@test.io: 27948216

定義身份驗證端點

此時,後端可以建立使用者、生成電子郵件令牌並透過電子郵件傳送。但是,生成的令牌仍然不具備功能。現在,您將透過建立/authenticate端點、根據資料庫驗證電子郵件令牌,並在authorization頭部中向用戶返回一個長期有效的 JWT 身份驗證令牌,從而實現身份驗證的第二步。

首先,將以下路由宣告新增到authPlugin

該路由需要emailemailToken。由於只有嘗試登入的合法使用者才知道兩者,因此猜測emailemailToken的難度增加,從而降低了猜測八位數字的暴力破解攻擊的風險。

接下來,將以下內容新增到auth.ts

注意: 環境變數JWT_SECRET可以透過執行以下命令生成:node -e "console.log(require('crypto').randomBytes(256).toString('base64'));"。在生產環境中應始終設定此變數。

處理程式從資料庫中獲取電子郵件令牌,確保其有效,然後在資料庫中建立一個新的 API 令牌,生成一個 JWT 令牌(包含對資料庫中令牌的引用),使電子郵件令牌失效,並在Authorization頭部中返回該令牌。

檢查點

  1. 使用npm run dev啟動伺服器
  2. 使用 curl 對/login端點進行 POST 呼叫:curl --header "Content-Type: application/json" --request POST --data '{"email":"test@test.io"}' localhost:3000/login。您應該看到後端記錄的令牌:email token for test@test.io: 13080740
  3. 獲取該令牌並使用 curl 呼叫/authenticate端點:curl -v --header "Content-Type: application/json" --request POST --data '{"email":"hello@prisma.io", "emailToken": "13080740"}' localhost:3000/authenticate
  4. 響應應具有200狀態,幷包含一個類似於此的Authorization頭部:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbklkIjoyOH0.gJYk3f1RJVKvPh75FdElCHwMoe_ZCMZftTE1Em5PpMg

定義身份驗證策略

身份驗證策略將定義 Hapi 如何驗證需要身份驗證的端點的請求。在此步驟中,您將透過使用 JWT 令牌中的tokenId從資料庫中獲取使用者資訊來定義使用 JWT 令牌驗證請求的邏輯。

要定義身份驗證策略,請將以下內容新增到auth.ts

authPlugin.register函式內部新增以下內容

最後,新增validateAPIToken函式

validateAPIToken函式將在使用API_AUTH_STATEGY的每個路由之前被呼叫(您已在前面的步驟中將其設定為預設值)。

validateAPIToken函式的作用是確定是否允許請求繼續進行。這是透過返回物件完成的,該物件包含isValidcredentials

  • isValid:確定令牌是否成功驗證。
  • credentials可用於將使用者資訊傳遞給請求物件。傳遞給credentials的物件可以透過request.auth.credentials在路由處理程式中訪問。

在這種情況下,我們確定如果令牌存在於資料庫中,並且有效且未過期,則我們獲取使用者所教授的課程(這將用於實現授權),並將其與tokenIduserIdisAdmin一起傳遞給憑證物件。

大多數端點都需要身份驗證(因為有預設的身份驗證策略),但仍然沒有授權規則。這意味著要訪問GET /courses端點,您現在需要在Authorization頭部中包含一個有效的 JWT 令牌。

檢查點

  1. 使用npm run dev啟動伺服器
  2. 使用 curl 對/courses端點進行 GET 呼叫:curl -v localhost:3000/courses。您應該收到 401 狀態碼,並附帶以下響應:{"statusCode":401,"error":"Unauthorized","message":"Missing authentication"}
  3. 使用上一個檢查點的令牌,再次使用Authorization頭部進行呼叫,如下所示:curl -H "Authorization:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbklkIjoyOH0.gJYk3f1RJVKvPh75FdElCHwMoe_ZCMZftTE1Em5PpMg" localhost:3000/courses,請求應該成功。

恭喜您,您已成功實現基於電子郵件的無密碼身份驗證並保護了端點。接下來,您將定義授權規則。

新增授權

後端的授權模型將定義使用者被允許做什麼。換句話說,他們被允許對哪些實體執行操作。

授予使用者許可權的主要屬性有

  • 使用者是否為管理員(由使用者模型中的isAdmin欄位指示)?如果是,他們將被允許執行所有操作。
  • 使用者是否為某門課程的老師?如果是,使用者將被允許對所有課程特定的資源(例如考試、考試結果和報名)執行 CRUD 操作。

如果使用者不是管理員或課程教師,他們仍然應該能夠建立新課程、作為學生報名現有課程、獲取其考試結果以及獲取和更新其使用者資料。

注意:這種方法混合了兩種授權方法,即基於角色和基於資源的授權。從課程報名中派生許可權是一種基於資源的授權形式。這意味著操作是基於特定資源授權的,即作為教師報名課程允許使用者建立相關測試並提交測試結果。另一方面,授權操作給管理員使用者(isAdmin設定為 true)是一種基於角色的授權形式,其中使用者具有“admin”角色。

端點的授權規則

為了實現所提出的授權規則,我們首先回顧一下包含所提出授權規則的端點列表

HTTP 方法路由描述授權規則POST/login開始登入/註冊併發送電子郵件令牌開放POST/authenticate驗證使用者並建立 JWT 令牌開放(需要電子郵件令牌)GET/profile獲取已驗證使用者資料任何已驗證使用者POST/users建立使用者僅限管理員GET/users/{userId}獲取使用者僅限管理員或已驗證使用者PUT/users/{userId}更新使用者僅限管理員或已驗證使用者DELETE/users/{userId}刪除使用者僅限管理員或已驗證使用者GET/users獲取使用者列表僅限管理員GET/users/{userId}/courses獲取使用者在課程中的報名情況僅限管理員或已驗證使用者POST/users/{userId}/courses將使用者報名到課程中(作為學生或老師)僅限管理員或已驗證使用者DELETE/users/{userId}/courses/{courseId}刪除使用者在課程中的報名僅限管理員或已驗證使用者POST/courses建立課程任何已驗證使用者GET/courses獲取課程列表任何已驗證使用者GET/courses/{courseId}獲取課程任何已驗證使用者PUT/courses/{courseId}更新課程僅限管理員或課程教師DELETE/courses/{courseId}刪除課程僅限管理員或課程教師POST/courses/{courseId}/tests為課程建立測試僅限管理員或課程教師GET/courses/tests/{testId}獲取測試任何已驗證使用者PUT/courses/tests/{testId}更新測試僅限管理員或課程教師DELETE/courses/tests/{testId}刪除測試僅限管理員或課程教師GET/users/{userId}/test-results獲取使用者的考試結果僅限管理員或已驗證使用者POST/courses/tests/{testId}/test-results為與使用者關聯的測試建立測試結果僅限管理員或課程教師GET/courses/tests/{testId}/test-results獲取測試的多個測試結果僅限管理員或課程教師PUT/courses/tests/test-results/{testResultId}更新測試結果(與使用者和測試關聯)僅限管理員或測試評分員DELETE/courses/tests/test-results/{testResultId}刪除測試結果僅限管理員或測試評分員

注意: 包含在{}中引數的路徑,例如{userId},表示 URL 中插入的變數,例如在www.myapi.com/users/13中,userId13

使用 Hapi 進行授權

Hapi 路由具有pre函式的概念,它允許將處理程式邏輯分解為更小、可重用的函式。pre函式在處理程式之前被呼叫,並允許接管響應並返回未經授權的錯誤。這在授權的上下文中非常有用,因為上面表格中提出的許多授權規則對於多個路由/端點將是相同的。例如,檢查使用者是否為管理員對於POST /usersGET /users路由都是相同的。這允許您重用一個isAdmin預函式並將其分配給這兩個端點。

為使用者端點新增授權

在這一部分中,您將定義pre函式來實現不同的授權規則。您將從三個/users/{userId}端點(GETPOSTDELETE)開始,如果發出請求的使用者是管理員或使用者正在請求其自己的userId,則應授權這些端點。

注意: Hapi 還提供了一種使用作用域(scopes)宣告性實現基於角色的身份驗證的方法。然而,所提出的基於資源的授權方法——即使用者的許可權取決於所請求的特定資源——需要更細粒度的控制,而這無法透過作用域實現,因此使用了pre函式。

要在GET /users/{userId}路由中新增一個預函式來驗證授權規則,請在src/plugins/user.ts中宣告以下函式

然後按如下方式將 pre 選項新增到src/plugins/user.ts中的路由定義中

現在,預函式將在getUserHandler之前被呼叫,並且僅授權管理員或請求其自己 userId 的使用者訪問。

注意: 在上一部分中,您已經定義了預設的身份驗證策略,因此嚴格來說不需要定義options.auth。但明確定義每個路由的身份驗證要求是一個好習慣。

檢查點: 為了驗證授權邏輯是否已正確實現,您將建立一個測試使用者和測試管理員,並呼叫/users/{userId}端點。

  1. 使用npm run dev啟動伺服器
  2. 執行seed-users指令碼建立測試使用者和測試管理員:npm run seed-users。您應該得到類似以下的結果
  1. 透過以下方式呼叫POST /login端點,以test@prisma.io身份登入
  1. 獲取記錄的令牌,然後使用 curl 呼叫/authenticate端點
  1. 響應應具有200狀態,幷包含一個類似於此的Authorization頭部:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbklkIjoyOH0.gJYk3f1RJVKvPh75FdElCHwMoe_ZCMZftTE1Em5PpMg
  2. 使用Authorization頭部包含上一個檢查點中的令牌,對/users/1(其中數字是在檢查點第一步中建立的測試使用者)進行 GET 呼叫,如下所示:curl -H "Authorization:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbklkIjoyOH0.gJYk3f1RJVKvPh75FdElCHwMoe_ZCMZftTE1Em5PpMg" localhost:3000/users/1,請求應該成功,您應該看到使用者資料。
  3. 使用相同的授權頭部對/users/2進行另一次 GET 呼叫:curl -H "Authorization:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbklkIjoyOH0.gJYk3f1RJVKvPh75FdElCHwMoe_ZCMZftTE1Em5PpMg" localhost:3000/users/2。這次應該以 403 禁止錯誤失敗。

如果所有步驟都成功,isRequestedUserOrAdmin預函式正確地授權使用者訪問其自己的使用者資料。要測試管理員功能,請從第三步重複,但以電子郵件test-admin@prisma.io作為測試管理員登入。管理員應該能夠獲取兩個使用者資料。

將授權預函式移動到單獨的模組

到目前為止,您已經定義了isRequestedUserOrAdmin授權預函式並將其新增到GET /users/{userId}路由。為了在不同的路由中使用它,請將該函式從src/plugins/users.ts移動到一個單獨的模組:src/auth-helpers.ts。該模組將允許您將授權邏輯集中組織在一個位置,並在不同外掛中定義的路由中重用它,例如user-enrollment.ts中的GET /users/{userId}/courses路由。

isRequestedUserOrAdmin函式移動到auth-helpers.ts後,將其作為預函式新增到以下具有相同授權邏輯的路由中

模組路由src/plugins/users.tsDELETE /users/{userId}src/plugins/users.tsPUT /users/{userId}src/plugins/users-enrollment.tsGET /users/{userId}/coursessrc/plugins/users-enrollment.tsPOST /users/{userId}/coursessrc/plugins/users-enrollment.tsDELETE /users/{userId}/coursessrc/plugins/test-results.tsGET /users/{userId}/test-results

為課程特定端點新增授權

教師應能夠更新他們所教授的課程以及為這些課程建立測試(如果他們是教師)和管理員。在此步驟中,您將建立另一個預函式來驗證這一點。

auth-helpers.ts中定義以下預函式

該預函式使用在validateAPIToken中獲取的teacherOf陣列來檢查使用者是否是所請求課程的教師。

isTeacherOfCourseOrAdmin作為預函式新增到以下路由

模組路由src/plugins/courses.tsPUT /courses/{courseId}src/plugins/courses.tsDELETE /courses/{courseId}src/plugins/tests.tsPOST /courses/{courseId}/tests

透過新增以下options.pre來更新表格中的路由

您現在已經實現了兩個不同的授權規則,並將其作為預函式新增到了後端十個不同的路由中。

更新測試

在 REST API 中實現身份驗證和授權後,測試將失敗,因為現在路由要求使用者進行身份驗證。在此步驟中,您將調整測試以考慮身份驗證。

例如,GET /users/{userId}端點有以下測試

如果您現在執行此測試,使用npm run test -- -t="get user returns user",測試將失敗。這是因為當請求到達端點時,它不滿足身份驗證要求。透過 Hapi 的server.inject——它模擬對伺服器的 HTTP 請求——您可以新增一個包含已驗證使用者資訊auth物件。該auth物件設定憑證物件,就像在src/plugins/auth.ts中的validateAPIToken函式中一樣,例如

傳入的credentials物件與src/plugins/auth.ts中定義的AuthCredentials介面匹配。

注意: TypeScript 中的介面與型別非常相似,但存在一些細微差別。要了解更多資訊,請檢視TypeScript 手冊

為了使測試透過,您將在測試中直接使用 Prisma 建立一個使用者,並按如下方式構建AuthCredentials物件

檢查點: 執行npm run test -- -t="get user returns user"以驗證測試是否透過。

至此,您已經修復了一個測試,但其他測試呢?由於在大多數測試中都需要建立憑證物件,您可以將其抽象到一個單獨的test-helpers.ts模組中。

下一步,編寫一個測試,驗證允許管理員使用GET /users/{userId}端點獲取不同使用者賬戶的授權規則。

總結與下一步

恭喜您走到這一步。本文涵蓋了許多概念,從身份驗證和授權概念開始,到使用 Prisma、Hapi 和 JWT 實現基於電子郵件的無密碼身份驗證。最後,您使用 Hapi 的預函式實現了授權規則。您還建立了一個電子郵件外掛,為後端提供了使用 SendGrid API 傳送電子郵件的能力。

身份驗證外掛封裝了身份驗證流程的兩個路由,並使用jwt身份驗證方案來定義身份驗證策略。在身份驗證策略的驗證函式中,您對照資料庫檢查了令牌,並使用與授權規則相關的資訊填充了憑證物件。

您還執行了資料庫遷移,並使用Prisma Migrate引入了一個新的Token表,該表與User表具有 n-1 關係。

TypeScript 幫助自動完成和驗證型別的正確使用(確保它們與資料庫 schema 同步)。

您廣泛使用了Prisma Client來從資料庫中獲取和持久化資料。

本文涵蓋了所有端點子集的授權。接下來的步驟,您可以執行以下操作

  • 遵循相同原則,為其餘路由新增授權。
  • 將憑證物件新增到所有測試中。
  • 生成並設定JWT_SECRET環境變數。
  • 設定SENDGRID_API_KEY環境變數並測試電子郵件功能。

您可以在GitHub上找到完整的原始碼,其中包含了所有端點的授權規則實現以及適配的測試。

雖然 Prisma 旨在簡化關係資料庫的操作,但理解底層資料庫和身份驗證原理仍然很有用。

如果您有問題,請隨時在Twitter上聯絡。

不要錯過下一篇文章!

訂閱 Prisma 新聞通訊

© . This site is unofficial and not affiliated with Prisma Data, Inc.