欢迎来到本系列关于使用 NestJS、Prisma 和 PostgreSQL 构建 REST API 的第五个教程!在本教程中,您将学习如何在 NestJS REST API 中实现 JWT 身份验证。
目录
简介
在本系列的上一章中,您学习了如何在 NestJS REST API 中处理关系型数据。您创建了一个 User
模型,并在 User
和 Article
模型之间添加了一个一对多关系。您还为 User
模型实现了 CRUD 端点。
在本章中,您将学习如何使用名为 Passport 的包向您的 API 添加身份验证。
在本教程中,您将使用上一章中构建的 API。
开发环境
要跟着本教程操作,您需要:
- ... 已安装 Node.js。
- ... 已安装 Docker 和 Docker 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
分支。
现在,执行以下操作以开始:
- 导航到克隆的目录
- 安装依赖项
- 使用 Docker 启动 PostgreSQL 数据库
- 应用数据库迁移
- 启动项目
注意:步骤 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
文件定义了数据库 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 提示。相应地回答问题:
您想为这个资源使用什么名称(复数,例如“users”)?
auth您使用什么传输层?
REST API您想生成 CRUD 入口点吗?
否
现在您应该在 src/auth
目录中找到一个新的 auth
模块。
安装和配置 passport
passport
是一个流行的 Node.js 应用程序身份验证库。它高度可配置,支持多种身份验证策略。它旨在与 NestJS 所基于的 Express Web 框架一起使用。NestJS 有一个与 passport
的第一方集成,称为 @nestjs/passport
,可以轻松地在您的 NestJS 应用程序中使用。
首先安装以下软件包:
现在您已经安装了所需的软件包,您可以在应用程序中配置 passport
。打开 src/auth.module.ts
文件并添加以下代码:
@nestjs/passport
模块提供了一个 PassportModule
,您可以将其导入到您的应用程序中。PassportModule
是 passport
库的封装,提供 NestJS 特定的实用程序。您可以在官方文档中阅读有关 PassportModule
的更多信息。
您还配置了一个 JwtModule
,您将使用它来生成和验证 JWT。JwtModule
是 jsonwebtoken
库的封装。secret
提供了一个用于签署 JWT 的密钥。expiresIn
对象定义了 JWT 的过期时间。它当前设置为 5 分钟。
注意:如果之前的令牌已过期,请记住生成一个新令牌。
您可以使用代码片段中显示的 jwtSecret
,也可以使用 OpenSSL 生成自己的密钥。
注意:在实际应用程序中,您绝不应将密钥直接存储在代码库中。NestJS 提供了
@nestjs/config
包,用于从环境变量加载密钥。您可以在官方文档中阅读有关它的更多信息。
实现 POST /auth/login
端点
POST /login
端点将用于验证用户身份。它将接受用户名和密码,并在凭据有效时返回 JWT。首先,您将创建一个 LoginDto
类,该类将定义请求主体的形状。
在 src/auth/dto
目录中创建一个名为 login.dto.ts
的新文件。
现在使用 email
和 password
字段定义 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。
在下一节中,您将使用此令牌来验证用户。
实现 JWT 身份验证策略
在 Passport 中,策略负责验证请求,它通过实现身份验证机制来完成此操作。在本节中,您将实现一个 JWT 身份验证策略,该策略将用于验证用户。
您不会直接使用 passport
包,而是与封装包 @nestjs/passport
进行交互,该包将在底层调用 passport
包。要使用 @nestjs/passport
配置策略,您需要创建一个继承 PassportStrategy
类的类。您需要在此类中完成两件主要事情:
- 您将在构造函数中将 JWT 策略特定的选项和配置传递给
super()
方法。 - 一个
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
。
为了使 UsersService
在 JwtStrategy
类中可访问,您还需要将其添加到 UsersModule
的 exports
中。
实现 JWT 认证守卫
守卫(Guards)是 NestJS 的一种构造,用于确定是否允许请求继续。在本节中,您将实现一个自定义 JwtAuthGuard
,它将用于保护需要身份验证的路由。
在 src/auth
目录中创建一个名为 jwt-auth.guard.ts
的新文件。
现在实现 JwtAuthGuard
类:
AuthGuard
类需要一个策略的名称。在此示例中,您使用的是在上一节中实现的 JwtStrategy
,其名称为 jwt
。
您现在可以将此守卫用作装饰器来保护您的端点。将 JwtAuthGuard
添加到 UsersController
中的路由。
如果您尝试在没有身份验证的情况下查询这些端点中的任何一个,它将不再起作用。
在 Swagger 中集成身份验证
目前,Swagger 上没有迹象表明这些端点受到身份验证保护。您可以向控制器添加 @ApiBearerAuth()
装饰器,以表明需要身份验证。
现在,受认证保护的端点在 Swagger 中应该有一个锁形图标 🔓
目前无法直接在 Swagger 中“验证”自己以测试这些端点。为此,您可以在 main.ts
中为 SwaggerModule
设置添加 .addBearerAuth()
方法调用。
您现在可以通过点击 Swagger 中的授权按钮来添加令牌。Swagger 会将令牌添加到您的请求中,这样您就可以查询受保护的端点。
注意:您可以通过向
/auth/login
端点发送POST
请求,并附带有效的password
来生成令牌。
自己试试看。
密码哈希
目前,User.password
字段以明文形式存储。这是一个安全风险,因为如果数据库被泄露,所有密码也会随之泄露。为了解决这个问题,您可以在将密码存储到数据库之前对其进行哈希处理。
您可以使用 bcrypt
加密库来哈希密码。使用 npm
安装它:
首先,您将更新 UsersService
中的 create
和 update
方法,以便在将密码存储到数据库之前对其进行哈希处理。
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