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 码。

由于 Web 应用程序难以验证卡片的持有情况,多因素身份验证通常通过补充用户名/密码和由身份验证器应用程序(安装在智能手机或生成这些密码的特殊设备上的应用程序)生成的一次性令牌来实现。

在本文中,您将实现基于电子邮件的无密码身份验证——这是一种两步方法,可以改善用户体验和安全性。它的工作原理是在尝试登录时向用户的电子邮件账户发送一个秘密令牌。一旦用户打开电子邮件并将令牌传递给应用程序,应用程序就可以对用户进行身份验证,并确定该用户是电子邮件账户的所有者。

这种方法依赖于用户的电子邮件服务,可以假定该服务已经验证了用户身份。用户体验得到改善,因为用户无需设置和记住密码。安全性得到增强,因为应用程序无需承担密码管理的责任,密码管理可能成为攻击面。

将身份验证外包给用户的电子邮件账户意味着应用程序将继承用户电子邮件账户安全的优势和劣势。但如今,大多数电子邮件服务都提供二次因素身份验证和其他安全措施的选项。

尽管如此,这种方法避免了用户选择弱密码,并可能在多个网站上重复使用。完全取消密码意味着这些用户更安全。不再有可以被猜测、暴力破解或完全破解的密码。

身份验证和注册/登录流程

基于电子邮件的无密码身份验证是一个两步过程,涉及两种令牌类型。

身份验证流程如下所示:

  1. 用户使用有效负载中的电子邮件调用 API 中的 /login 端点,以开始身份验证过程。
  2. 如果电子邮件是新的,则在用户表中创建用户。
  3. 后端生成一个电子邮件令牌并将其保存在令牌表中
  4. 电子邮件令牌发送到用户的电子邮件地址
  5. 用户将电子邮件令牌(通过电子邮件收到)和电子邮件地址发送到 /authenticate 端点
  6. 后端验证用户发送的电子邮件令牌。如果有效且令牌未过期,则生成一个 JWT 令牌并将其保存在令牌表中。
  7. JWT 令牌通过 Authorization 头部发送回用户。

有两种令牌类型:

  1. 电子邮件令牌:一个八位数的数字令牌,有效期较短,例如 10 分钟,发送到用户的电子邮件。令牌的唯一目的是验证用户与该电子邮件关联,这意味着它不授予访问任何评分应用相关端点的权限。
  2. 身份验证令牌:一个 JWT 令牌,其有效负载中包含 tokenId。此令牌可用于访问受保护的端点,只需在向 API 发送请求时将其放在 Authorization 头部中即可。该令牌是长期有效的,因为它在 12 小时内有效。

通过这种身份验证策略,单个端点处理登录和注册。这是可能的,因为登录和注册的唯一区别在于您是否在“用户”表中创建一行(如果用户已经存在)。

JSON Web Tokens

JSON Web Tokens (JWT) 是一种开放且标准的方法,用于安全地表示双方之间的声明。该标准定义了一种紧凑且自包含的方式,用于将信息以 JSON 对象的形式安全地传输给各方。这些信息是经过数字签名的,因此可以验证和信任。

一个 JWT 令牌包含三个部分,它们使用 Base64 进行编码:头部 (header)有效负载 (payload)签名 (signature),结构如下(各部分用 . 分隔):

注意:Base64 是另一种表示数据的方式。它不涉及任何加密

如果您使用 Base64 解码上述头部和有效负载,您将得到以下内容:

  • 头部: {"alg":"HS256","typ":"JWT"}
  • 有效负载: {"tokenId":9}

令牌的签名部分是通过将头部有效负载密钥通过签名算法(在本例中为 HS256)生成。密钥只有后端知道,用于验证令牌的真实性。

在本文中,JWT 将用于长期有效的身份验证令牌。令牌的有效负载将包含 tokenId,它将存储在数据库中并引用创建该令牌的用户。这允许后端查找关联的用户。

注意:这种方法称为有状态 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 的新文件

并在文件中添加以下内容:

注意:auth 插件定义了对 prismahapi-auth-jwt2app/email 插件的依赖关系。prisma 插件在系列第二部分中定义,将用于访问 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 令牌验证请求的逻辑,通过使用 JWT 令牌中的 tokenId 从数据库中获取用户信息。

要定义认证策略,请将以下内容添加到 auth.ts

authPlugin.register 函数内部添加以下内容。

最后,添加 validateAPIToken 函数。

validateAPIToken 函数将在使用 API_AUTH_STATEGY 的每个路由(您在上一步中已将其设置为默认值)之前被调用。

validateAPIToken 函数的目的是确定是否允许请求继续。这是通过返回对象完成的,该对象包含 isValidcredentials

  • isValid:确定令牌是否成功验证。
  • credentials 可用于将用户信息传递给请求对象。传递给 credentials 的对象可通过路由处理程序中的 request.auth.credentials 访问。

在本例中,我们确定如果令牌存在于数据库中,则其有效且未过期。如果有效,我们将获取用户是教师的课程(这将用于实现授权),并将其与 tokenIduserIdisAdmin 一起传递给 credentials 对象。

大多数端点都需要认证(因为采用了默认认证策略),但目前还没有授权规则。这意味着要访问 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-函数并将其分配给这两个端点。

为用户端点添加授权

在这一部分,您将定义 pre 函数来实现不同的授权规则。您将从三个 /users/{userId} 端点(GETPOSTDELETE)开始,如果发出请求的用户是管理员或用户正在请求其自己的 userId,则应该授权这些端点。

注意: Hapi 也提供了一种使用 scopes 声明性地实现基于角色的认证的方法。然而,建议的基于资源的授权方法——用户的权限取决于请求的特定资源——需要更细粒度的控制,这无法通过 scopes 实现,因此使用 pre 函数。

要在 GET /users/{userId} 路由中添加一个用于验证授权规则的 pre-函数,请在 src/plugins/user.ts 中声明以下函数。

然后将 pre 选项添加到 src/plugins/user.ts 中的路由定义中,如下所示。

pre-函数现在将在 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 Forbidden 错误而失败。

如果所有步骤都成功,isRequestedUserOrAdmin pre-函数已正确授权用户访问其自己的用户个人资料。要测试管理员功能,请从第三步开始重复,但以电子邮件 test-admin@prisma.io 的测试管理员身份登录。管理员应该能够获取两个用户个人资料。

将授权 pre-函数移至单独的模块

到目前为止,您已经定义了 isRequestedUserOrAdmin 授权 pre-函数并将其添加到 GET /users/{userId} 路由。为了在不同的路由中使用此函数,请将函数从 src/plugins/users.ts 移至单独的模块:src/auth-helpers.ts。此模块将允许您将授权逻辑组织在一个地方,并将其重用于不同插件中定义的路由,例如 user-enrollment.ts 中的 GET /users/{userId}/courses 路由。

isRequestedUserOrAdmin 函数移至 auth-helpers.ts 后,将其作为 pre-函数添加到具有相同授权逻辑的以下路由中。

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

为课程特定端点添加授权

教师应该能够更新他们是教师的课程以及为这些课程创建测试,管理员也应如此。在此步骤中,您将创建另一个 pre-函数来验证这一点。

auth-helpers.ts 中定义以下 pre-函数。

pre-函数使用在 validateAPIToken 中获取的 teacherOf 数组来检查用户是否是请求课程的教师。

isTeacherOfCourseOrAdmin 作为 pre-函数添加到以下路由。

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

通过添加以下 options.pre 来更新表中的路由。

您现在已经实现了两种不同的授权规则,并将它们作为 pre-函数添加到了后端十个不同的路由中。

更新测试

在 REST API 中实现了认证和授权后,测试将会失败,因为路由现在要求用户经过认证。在此步骤中,您将调整测试以考虑认证。

例如,GET /users/{userId} 端点有以下测试。

如果您现在运行此测试:npm run test -- -t="get user returns user",测试将会失败。这是因为当请求到达端点时,它不满足其认证要求。使用 Hapi 的 server.inject——模拟对服务器的 HTTP 请求——您可以添加一个包含已认证用户信息 auth 对象。auth 对象设置 credentials 对象的方式与 src/plugins/auth.ts 中的 validateAPIToken 函数相同,例如。

传递的 credentials 对象与 src/plugins/auth.ts 中定义的 AuthCredentials 接口匹配。

注意: TypeScript 中的接口与类型非常相似,但存在一些细微差别。要了解更多信息,请查阅 TypeScript Handbook

为了让测试通过,您将在测试中直接使用 Prisma 创建用户,并按如下方式构建 AuthCredentials 对象。

检查点: 运行 npm run test -- -t="get user returns user" 以验证测试是否通过。

此时,您已经修复了一个测试,但其他测试呢?由于大多数测试都需要创建 credentials 对象,您可以将其抽象到一个单独的 test-helpers.ts 模块中。

下一步,编写一个测试来验证允许管理员使用 GET /users/{userId} 端点获取不同用户账户的授权规则。

总结与下一步

恭喜您走到这里。本文涵盖了许多概念,从认证和授权概念开始,到使用 Prisma、Hapi 和 JWT 实现基于电子邮件的无密码认证。最后,您使用 Hapi 的 pre-函数实现了授权规则。您还创建了一个电子邮件插件,使后端能够使用 SendGrid 的 API 发送电子邮件。

auth 插件封装了认证流程的两个路由,并使用了 jwt 认证方案来定义认证策略。在认证策略的 validate 函数中,您针对数据库检查了令牌,并使用与授权规则相关的信息填充了 credentials 对象。

您还执行了数据库迁移,并使用 Prisma Migrate 引入了一个新的 Token 表,与 User 表之间存在 n-1 关系。

TypeScript 帮助自动完成并验证类型的正确使用(确保它们与数据库模式同步)。

您广泛使用了 Prisma Client 来在数据库中获取和持久化数据。

本文涵盖了所有端点的一个子集的授权。下一步,您可以执行以下操作。

  • 遵循相同的原则,为其余路由添加授权。
  • 将 credential 对象添加到所有测试中。
  • 生成并设置 JWT_SECRET 环境变量。
  • 设置 SENDGRID_API_KEY 环境变量并测试电子邮件功能。

您可以在 GitHub 上找到完整的源代码,其中包含了所有已实现端点的授权规则,并且测试也已进行了调整。

虽然 Prisma 旨在简化关系型数据库的操作,但理解底层数据库和认证原理仍然很有用。

如果您有任何问题,请随时在 Twitter 上联系我。

不要错过下一篇文章!

订阅 Prisma Newsletter