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 并为数据库播种。

现在,您应该能够访问 http://localhost: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 Tokens (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 端点。

转到 http://localhost: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 标头中提供承载令牌的标准方法。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 类中使用。

为了使 UsersServiceJwtStrategy 类中可访问,您还需要将其添加到 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 会将令牌添加到您的请求中,以便您可以查询受保护的端点。

注意:您可以通过向 /auth/login 端点发送带有有效 emailpasswordPOST 请求来生成令牌。

自己尝试一下。

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 新闻通讯