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

由于网页应用程序难以验证卡片所有权,因此多因素身份验证通常通过使用身份验证器应用程序(安装在智能手机或专用设备上生成这些密码的应用程序)生成的一次性令牌来补充用户名/密码。

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

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

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

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

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

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

身份验证流程如下

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

有两种令牌类型

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

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

JSON Web 令牌

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

一个 JWT 令牌包含三个用 Base64 编码的部分:header(头部)payload(载荷)signature(签名),其格式如下(各部分用.分隔)

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

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

  • 头部:{"alg":"HS256","typ":"JWT"}
  • 载荷:{"tokenId":9}

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

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

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

并将以下内容添加到文件中

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

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

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

最后,添加validateAPIToken函数

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

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

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

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

大多数端点都需要身份验证(因为有默认的身份验证策略),但仍然没有授权规则。这意味着要访问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函数来实现不同的授权规则。您将从三个/users/{userId}端点(GETPOSTDELETE)开始,如果发出请求的用户是管理员或用户正在请求其自己的userId,则应授权这些端点。

注意: Hapi 还提供了一种使用作用域(scopes)声明性实现基于角色的身份验证的方法。然而,所提出的基于资源的授权方法——即用户的权限取决于所请求的特定资源——需要更细粒度的控制,而这无法通过作用域实现,因此使用了pre函数。

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

然后按如下方式将 pre 选项添加到src/plugins/user.ts中的路由定义中

现在,预函数将在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 禁止错误失败。

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

将授权预函数移动到单独的模块

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

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

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

为课程特定端点添加授权

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

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

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

isTeacherOfCourseOrAdmin作为预函数添加到以下路由

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

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

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

更新测试

在 REST API 中实现身份验证和授权后,测试将失败,因为现在路由要求用户进行身份验证。在此步骤中,您将调整测试以考虑身份验证。

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

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

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

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

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

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

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

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

总结与下一步

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

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

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

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

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

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

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

您可以在GitHub上找到完整的源代码,其中包含了所有端点的授权规则实现以及适配的测试。

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

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

不要错过下一篇文章!

订阅 Prisma 新闻通讯

© . All rights reserved.