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 添加了一些非常不錯的智慧感知和語法高亮。
  • ... 可選擁有 Unix shell 訪問許可權(例如 Linux 和 macOS 中的終端/shell)以執行本系列中提供的命令。

如果您沒有 Unix shell(例如,您使用的是 Windows 機器),您仍然可以跟著操作,但 shell 命令可能需要根據您的機器進行修改。

克隆倉庫

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

本教程的起點可在 GitHub 倉庫end-validation 分支中找到。要開始,請克隆倉庫並檢出 end-validation 分支。

現在,執行以下操作以開始:

  1. 導航到克隆的目錄
  1. 安裝依賴項
  1. 使用 Docker 啟動 PostgreSQL 資料庫
  1. 應用資料庫遷移
  1. 啟動專案

注意:步驟 4 也將生成 Prisma Client 併為資料庫填充資料。

現在,您應該能夠透過 https://:3000/api/ 訪問 API 文件。

專案結構和檔案

您克隆的倉庫應具有以下結構:

注意:您可能會注意到此資料夾還帶有一個 test 目錄。本教程不會涵蓋測試。但是,如果您想了解使用 Prisma 測試應用程式的最佳實踐,請務必檢視本系列教程:《Prisma 測試終極指南》。

此倉庫中值得注意的檔案和目錄是:

  • src 目錄包含應用程式的原始碼。共有三個模組:
    • app 模組位於 src 目錄的根部,是應用程式的入口點。它負責啟動 Web 伺服器。
    • prisma 模組包含 Prisma Client,它是您與資料庫的介面。
    • articles 模組定義了 /articles 路由的端點和附帶的業務邏輯。
    • users 模組定義了 /users 路由的端點和附帶的業務邏輯。
  • prisma 資料夾包含以下內容:
    • schema.prisma 檔案定義了資料庫 schema。
    • 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 Tokens (JWT) 實現基於令牌的身份驗證。

注意這個短影片解釋了兩種身份驗證的基礎知識。

首先,在您的應用程式中建立一個新的 auth 模組。執行以下命令生成一個新模組:

您將收到一些 CLI 提示。相應地回答問題:

  1. 您想為這個資源使用什麼名稱(複數,例如“users”)? auth
  2. 您使用什麼傳輸層? REST API
  3. 您想生成 CRUD 入口點嗎?

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

安裝和配置 passport

passport 是一個流行的 Node.js 應用程式身份驗證庫。它高度可配置,支援多種身份驗證策略。它旨在與 NestJS 所基於的 Express Web 框架一起使用。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://: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 類:

您已經建立了一個繼承自 PassportStrategy 類的 JwtStrategy 類。PassportStrategy 類接受兩個引數:一個策略實現和策略的名稱。這裡您使用的是 passport-jwt 庫中預定義的策略。

您正在向建構函式中的 super() 方法傳遞一些選項。jwtFromRequest 選項需要一個可用於從請求中提取 JWT 的方法。在這種情況下,您將使用標準方法:在 API 請求的 Authorization 頭部中提供一個 Bearer 令牌。secretOrKey 選項告訴策略使用哪個金鑰來驗證 JWT。還有更多選項,您可以在 passport-jwt 倉庫中閱讀更多資訊。

對於 passport-jwt,Passport 首先驗證 JWT 的簽名並解碼 JSON。然後將解碼後的 JSON 傳遞給 validate() 方法。根據 JWT 簽名的工作方式,您保證收到一個之前由您的應用程式簽名和釋出的有效令牌。validate() 方法應該返回一個使用者物件。如果未找到使用者,validate() 方法將丟擲錯誤。

注意:Passport 可能相當令人困惑。將其視為一個微型框架會很有幫助,它將身份驗證過程抽象為幾個步驟,這些步驟可以透過策略和配置選項進行自定義。我建議閱讀 NestJS Passport 配方以瞭解如何將 Passport 與 NestJS 一起使用。

將新的 JwtStrategy 作為提供者新增到 AuthModule 中。

現在,JwtStrategy 可以被其他模組使用。您還在 imports 中添加了 UsersModule,因為 JwtStrategy 類中使用了 UsersService

為了使 UsersServiceJwtStrategy 類中可訪問,您還需要將其新增到 UsersModuleexports 中。

實現 JWT 認證守衛

守衛(Guards)是 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 中“驗證”自己以測試這些端點。為此,您可以在 main.ts 中為 SwaggerModule 設定新增 .addBearerAuth() 方法呼叫。

您現在可以透過點選 Swagger 中的授權按鈕來新增令牌。Swagger 會將令牌新增到您的請求中,這樣您就可以查詢受保護的端點。

注意:您可以透過向 /auth/login 端點發送 POST 請求,並附帶有效的 emailpassword 來生成令牌。

自己試試看。

Authentication workflow in Swagger

密碼雜湊

目前,User.password 欄位以明文形式儲存。這是一個安全風險,因為如果資料庫被洩露,所有密碼也會隨之洩露。為了解決這個問題,您可以在將密碼儲存到資料庫之前對其進行雜湊處理。

您可以使用 bcrypt 加密庫來雜湊密碼。使用 npm 安裝它:

首先,您將更新 UsersService 中的 createupdate 方法,以便在將密碼儲存到資料庫之前對其進行雜湊處理。

bcrypt.hash 函式接受兩個引數:雜湊函式的輸入字串和雜湊的輪數(也稱為成本因子)。增加雜湊輪數會增加計算雜湊所需的時間。這裡在安全性和效能之間存在權衡。更多的雜湊輪數意味著計算雜湊需要更多時間,這有助於防止暴力破解攻擊。然而,更多的雜湊輪數也意味著使用者登入時計算雜湊需要更多時間。這個 Stack Overflow 回答對這個話題有很好的討論。

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

您還需要更新資料庫種子指令碼,以便在將密碼插入資料庫之前對其進行雜湊處理。

執行 npx prisma db seed 命令後,您應該會看到資料庫中儲存的密碼現在已經被雜湊了。

由於每次使用的鹽值不同,您的 password 欄位的值也會不同。重要的是,現在該值是一個雜湊字串。

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

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

總結和最終說明

在本章中,您學習瞭如何在 NestJS REST API 中實現 JWT 身份驗證。您還學習了密碼加鹽以及將身份驗證與 Swagger 整合。

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

不要錯過下一篇文章!

訂閱 Prisma Newsletter

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