2023 年 3 月 31 日

使用 NestJS 和 Prisma 建構 REST API:身份驗證

10 分鐘閱讀

歡迎來到本系列關於使用 NestJS、Prisma 和 PostgreSQL 建構 REST API 的第五篇教學!在本教學中,您將學習如何在您的 NestJS REST API 中實作 JWT 身份驗證。

Building a REST API with NestJS and Prisma: Authentication

目錄

簡介

在本系列的前一章中,您學習了如何在您的 NestJS REST API 中處理關聯式資料。您建立了一個 User 模型,並在 UserArticle 模型之間新增了一對多的關係。您也為 User 模型實作了 CRUD 端點。

在本章中,您將學習如何使用一個名為 Passport 的套件,將身份驗證新增到您的 API。

  1. 首先,您將使用一個名為 Passport 的函式庫,實作基於 JSON Web Token (JWT) 的身份驗證。
  2. 接下來,您將使用 bcrypt 函式庫雜湊密碼,以保護儲存在資料庫中的密碼。

在本教學中,您將使用在上一章中建構的 API。

開發環境

為了跟上本教學,您需要

  • ... 已安裝 Node.js
  • ... 已安裝 DockerDocker Compose。如果您使用 Linux,請確保您的 Docker 版本為 20.10.0 或更高版本。您可以透過在終端機中執行 docker version 來檢查您的 Docker 版本。
  • ... 選擇性地 安裝 Prisma VS Code 擴充功能。Prisma VS Code 擴充功能為 Prisma 新增了一些非常棒的 IntelliSense 和語法突顯功能。
  • ... 選擇性地 存取 Unix shell(例如 Linux 和 macOS 中的終端機/shell),以執行本系列中提供的指令。

如果您沒有 Unix shell(例如,您使用的是 Windows 電腦),您仍然可以跟上,但 shell 指令可能需要針對您的電腦進行修改。

複製儲存庫

本教學的起點是本系列第二章的結尾。它包含一個使用 NestJS 建構的基本 REST API。

本教學的起點可在 end-validation 分支的 GitHub 儲存庫 中找到。若要開始使用,請複製儲存庫並簽出 end-validation 分支

現在,執行下列動作以開始使用

  1. 導覽至複製的目錄
  1. 安裝依賴項
  1. 使用 Docker 啟動 PostgreSQL 資料庫
  1. 套用資料庫遷移
  1. 啟動專案

注意:步驟 4 也會產生 Prisma Client 並植入資料庫。

現在,您應該可以透過 https://127.0.0.1:3000/api/ 存取 API 文件。

專案結構和檔案

您複製的儲存庫應該具有下列結構

注意:您可能會注意到此資料夾也隨附 test 目錄。本教學不會涵蓋測試。但是,如果您想了解有關使用 Prisma 測試應用程式的最佳實務,請務必查看本教學系列:Prisma 測試終極指南

此儲存庫中值得注意的檔案和目錄為

  • src 目錄包含應用程式的原始碼。共有三個模組
    • app 模組位於 src 目錄的根目錄,是應用程式的進入點。它負責啟動 Web 伺服器。
    • prisma 模組包含 Prisma Client,您的資料庫介面。
    • articles 模組定義 /articles 路由和隨附的業務邏輯的端點。
    • users 模組定義 /users 路由和隨附的業務邏輯的端點。
  • prisma 資料夾具有下列項目
    • schema.prisma 檔案定義資料庫結構描述。
    • migrations 目錄包含資料庫遷移歷史記錄。
    • seed.ts 檔案包含一個指令碼,用於使用虛擬資料植入您的開發資料庫。
  • docker-compose.yml 檔案定義您的 PostgreSQL 資料庫的 Docker 映像。
  • .env 檔案包含您的 PostgreSQL 資料庫的資料庫連線字串。

注意:如需有關這些元件的詳細資訊,請瀏覽本教學系列第一章

在您的 REST API 中實作身份驗證

在本節中,您將為您的 REST API 實作大部分的身份驗證邏輯。在本節結束時,下列端點將受到身份驗證保護 🔒

  • GET /users
  • GET /users/:id
  • PATCH /users/:id
  • DELETE /users/:id

Web 上使用兩種主要的身份驗證類型:基於會話的身份驗證和基於權杖的身份驗證。在本教學中,您將使用 JSON Web Token (JWT) 實作基於權杖的身份驗證。

注意這段短片說明了這兩種身份驗證的基本知識。

若要開始使用,請在您的應用程式中建立一個新的 auth 模組。執行下列指令以產生新的模組

系統會提供您一些 CLI 提示。請據實回答問題

  1. 您想要為此資源使用什麼名稱(複數,例如「users」)? auth
  2. 您使用什麼傳輸層? REST API
  3. 您想要產生 CRUD 進入點嗎?

您現在應該在 src/auth 目錄中找到一個新的 auth 模組。

安裝和設定 passport

passport 是 Node.js 應用程式常用的身份驗證函式庫。它具有高度可設定性,並支援各種身份驗證策略。它旨在與 Express Web 架構搭配使用,而 NestJS 正是建立在其之上。NestJS 與 passport 有第一方整合,稱為 @nestjs/passport,可讓您輕鬆地在 NestJS 應用程式中使用它。

首先安裝下列套件

現在您已安裝必要的套件,您可以在您的應用程式中設定 passport。開啟 src/auth.module.ts 檔案並新增下列程式碼

@nestjs/passport 模組提供了一個 PassportModule,您可以將其匯入您的應用程式。PassportModulepassport 函式庫的包裝函式,提供 NestJS 特定的實用程式。您可以在官方文件中閱讀有關 PassportModule 的詳細資訊。

您也設定了一個 JwtModule,您將使用它來產生和驗證 JWT。JwtModulejsonwebtoken 函式庫的包裝函式。secret 提供了一個私密金鑰,用於簽署 JWT。expiresIn 物件定義 JWT 的到期時間。目前設定為 5 分鐘。

注意:如果先前的權杖已過期,請記得產生新的權杖。

您可以使用程式碼片段中顯示的 jwtSecret,或使用 OpenSSL 產生您自己的金鑰。

注意:在實際應用程式中,您絕不應該將私密金鑰直接儲存在您的程式碼庫中。NestJS 提供了 @nestjs/config 套件,用於從環境變數載入私密金鑰。您可以在官方文件中閱讀有關它的詳細資訊。

實作 POST /auth/login 端點

POST /login 端點將用於驗證使用者身份。它將接受使用者名稱和密碼,如果憑證有效,則傳回 JWT。首先,您建立一個 LoginDto 類別,它將定義請求主體的形狀。

src/auth/dto 目錄中建立一個名為 login.dto.ts 的新檔案

現在使用 emailpassword 欄位定義 LoginDto 類別

您也需要定義一個新的 AuthEntity,它將描述 JWT 酬載的形狀。在 src/auth/entity 目錄中建立一個名為 auth.entity.ts 的新檔案

現在在此檔案中定義 AuthEntity

AuthEntity 只有一個名為 accessToken 的字串欄位,它將包含 JWT。

現在在 AuthService 內部建立一個新的 login 方法

login 方法首先擷取具有給定電子郵件的使用者。如果找不到使用者,則會擲回 NotFoundException。如果找到使用者,它會檢查密碼是否正確。如果密碼不正確,則會擲回 UnauthorizedException。如果密碼正確,它會產生一個包含使用者 ID 的 JWT 並傳回它。

現在在 AuthController 內部建立 POST /auth/login 方法

現在您的 API 中應該有一個新的 POST /auth/login 端點。

前往 https://127.0.0.1:3000/api 頁面並嘗試 POST /auth/login 端點。提供您在植入指令碼中建立的使用者的憑證

您可以使用下列請求主體

執行請求後,您應該會在回應中取得 JWT。

POST /auth/login endpoint

在下一節中,您將使用此權杖來驗證使用者身份。

實作 JWT 身份驗證策略

在 Passport 中,策略負責驗證請求,它透過實作身份驗證機制來完成此任務。在本節中,您將實作 JWT 身份驗證策略,它將用於驗證使用者身份。

您將不會直接使用 passport 套件,而是與包裝函式套件 @nestjs/passport 互動,它會在幕後呼叫 passport 套件。若要使用 @nestjs/passport 設定策略,您需要建立一個擴充 PassportStrategy 類別的類別。您需要在這個類別中執行兩項主要工作

  1. 您將 JWT 策略特定的選項和設定傳遞至建構函式中的 super() 方法。
  2. validate() 回呼方法,它將與您的資料庫互動,以根據 JWT 酬載擷取使用者。如果找到使用者,則預期 validate() 方法會傳回使用者物件。

首先在 src/auth/strategy 目錄中建立一個名為 jwt.strategy.ts 的新檔案

現在實作 JwtStrategy 類別

您已建立一個 JwtStrategy 類別,它擴充了 PassportStrategy 類別。PassportStrategy 類別接受兩個引數:策略實作和策略名稱。在這裡,您使用的是來自 passport-jwt 函式庫的預先定義策略。

您正在將一些選項傳遞至建構函式中的 super() 方法。jwtFromRequest 選項預期一個方法,可用於從請求中擷取 JWT。在本例中,您將使用在 API 請求的授權標頭中提供持有者權杖的標準方法。secretOrKey 選項會告知策略使用什麼私密金鑰來驗證 JWT。還有許多其他選項,您可以在 passport-jwt 儲存庫中閱讀有關它們的詳細資訊。

對於 passport-jwt,Passport 首先會驗證 JWT 的簽章並解碼 JSON。然後,解碼的 JSON 會傳遞至 validate() 方法。根據 JWT 簽署方式,您可以保證收到先前由您的應用程式簽署和發出的有效權杖。validate() 方法預期傳回使用者物件。如果找不到使用者,則 validate() 方法會擲回錯誤。

注意:Passport 可能相當令人困惑。將 Passport 視為一個迷你架構本身會很有幫助,它可以將身份驗證程序抽象化為幾個步驟,這些步驟可以使用策略和設定選項進行自訂。我建議閱讀 NestJS Passport 食譜,以了解更多關於如何將 Passport 與 NestJS 搭配使用的資訊。

將新的 JwtStrategy 新增為 AuthModule 中的提供者

現在 JwtStrategy 可以被其他模組使用。您也在 imports 中新增了 UsersModule,因為 UsersService 正用於 JwtStrategy 類別中。

若要讓 UsersService 可在 JwtStrategy 類別中存取,您也需要在 UsersModuleexports 中新增它

實作 JWT 身份驗證守衛

守衛是一種 NestJS 建構,用於判斷是否應允許請求繼續進行。在本節中,您將實作一個自訂 JwtAuthGuard,它將用於保護需要身份驗證的路由。

src/auth 目錄中建立一個名為 jwt-auth.guard.ts 的新檔案

現在實作 JwtAuthGuard 類別

AuthGuard 類別預期策略的名稱。在本例中,您使用的是您在上一節中實作的 JwtStrategy,其名稱為 jwt

您現在可以使用此守衛作為裝飾器來保護您的端點。將 JwtAuthGuard 新增至 UsersController 中的路由

如果您嘗試在沒有身份驗證的情況下查詢任何這些端點,它將不再運作。

`GET /users endpoint gives 401 response

將身份驗證整合到 Swagger 中

目前在 Swagger 上沒有任何指示表明這些端點受到身份驗證保護。您可以將 @ApiBearerAuth() 裝飾器新增至控制器,以指示需要身份驗證

現在,受身份驗證保護的端點在 Swagger 中應該有一個鎖圖示 🔓

Auth protected endpoints in Swagger

目前無法直接在 Swagger 中「驗證」您自己的身份,以便您可以測試這些端點。若要執行此操作,您可以將 .addBearerAuth() 方法呼叫新增至 main.ts 中的 SwaggerModule 設定

您現在可以透過按一下 Swagger 中的 Authorize 按鈕來新增權杖。Swagger 會將權杖新增至您的請求,以便您可以查詢受保護的端點。

注意:您可以透過將 POST 請求傳送至具有有效 emailpassword/auth/login 端點來產生權杖。

親自試試看。

Authentication workflow in Swagger

雜湊密碼

目前,User.password 欄位以純文字形式儲存。這是一個安全風險,因為如果資料庫遭到入侵,所有密碼也會遭到入侵。若要修正此問題,您可以在將密碼儲存在資料庫之前先雜湊密碼。

您可以使用 bcrypt 密碼編譯函式庫來雜湊密碼。使用 npm 安裝它

首先,您將更新 UsersService 中的 createupdate 方法,以在將密碼儲存在資料庫之前先雜湊密碼

bcrypt.hash 函式接受兩個引數:雜湊函式的輸入字串和雜湊輪數(也稱為成本因子)。增加雜湊輪數會增加計算雜湊所需的時間。這裡需要在安全性和效能之間權衡。雜湊輪數越多,計算雜湊所需的時間就越長,這有助於防止暴力破解攻擊。但是,雜湊輪數越多也表示使用者登入時計算雜湊所需的時間越長。這個 stack overflow 回答對此主題進行了很好的討論。

bcrypt 也會自動使用另一種稱為加鹽的技術,以使其更難以暴力破解雜湊。加鹽是一種在雜湊之前將隨機字串新增至輸入字串的技術。這樣,攻擊者就無法使用預先計算的雜湊表來破解密碼,因為每個密碼都有不同的鹽值。

您也需要更新您的資料庫植入指令碼,以在將密碼插入資料庫之前先雜湊密碼

使用 npx prisma db seed 執行植入指令碼,您應該會看到儲存在資料庫中的密碼現在已雜湊。

password 欄位的值對您來說會有所不同,因為每次都會使用不同的鹽值。重要的是,該值現在是一個雜湊字串。

現在,如果您嘗試使用正確的密碼 login,您將會遇到 HTTP 401 錯誤。這是因為 login 方法嘗試將使用者請求中的純文字密碼與資料庫中的雜湊密碼進行比較。更新 login 方法以使用雜湊密碼

您現在可以使用正確的密碼登入,並在回應中取得 JWT。

摘要與結語

在本章中,您學習了如何在您的 NestJS REST API 中實作 JWT 身份驗證。您也學習了有關加鹽密碼以及將身份驗證與 Swagger 整合的資訊。

您可以在 end-authentication 分支的 GitHub 儲存庫中找到本教學的完成程式碼。如果您發現問題,請隨時在儲存庫中提出問題或提交 PR。您也可以直接在 Twitter 上與我聯繫。

別錯過下一篇文章!

訂閱 Prisma 電子報