2020年9月10日

使用 TypeScript、PostgreSQL 和 Prisma 的后端:身份验证和授权

在本系列的第三部分中,我们将探讨如何使用 Prisma 进行令牌存储,通过无密码身份验证来保护 REST API,并实现授权。

Backend with TypeScript, PostgreSQL & Prisma: Authentication & Authz

简介

本系列的目标是通过解决一个具体问题:在线课程的评分系统,来探索和展示现代后端的不同模式、问题和架构。这是一个很好的例子,因为它具有多样的关系类型,并且足够复杂以代表真实的用例。

上面提供了直播录像,内容与本文相同。

本系列将涵盖的内容

本系列将重点关注数据库在后端开发各个方面的作用,涵盖

主题部分
数据建模第一部分
CRUD第一部分
聚合第一部分
REST API 层第二部分
验证第二部分
测试第二部分
无密码身份验证第三部分(当前)
授权第三部分(当前)
与外部 API 集成第三部分(当前)
部署即将推出

您今天将学到的内容

在第一篇文章中,您为问题域设计了一个数据模型,并编写了一个种子脚本,该脚本使用 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 密钥,生成一个 API 密钥,并将其保存在安全的地方。

克隆存储库

本系列的源代码可以在 GitHub 上找到。

要开始使用,请克隆存储库并安装依赖项

注意:通过检出 part-3 分支,您将能够从相同的起点开始学习本文。

启动 PostgreSQL

要启动 PostgreSQL,请从 real-world-grading-app 文件夹运行以下命令

注意:Docker 将使用 docker-compose.yml 文件来启动 PostgreSQL 容器。

身份验证和授权概念

在深入实施之前,我们将介绍一些与身份验证和授权相关的概念。

虽然这两个术语经常互换使用,但身份验证和授权服务于不同的目的。一般来说,它们都以互补的方式用于保护应用程序。

简单来说,身份验证是验证用户是谁的过程,而授权是验证他们有权访问什么的过程。

现实世界中身份验证的一个例子是有效的护照。您看起来像官方文件(难以伪造)中的人这一事实验证了您是您声称的那个人。例如,当您去机场时,您出示护照,然后被允许通过安检。

在同一个例子中,授权是您被允许登机的过程:您出示登机牌(通常会扫描并对照航班乘客数据库进行验证),地勤人员授权您登机。

Web 应用程序中的身份验证

Web 应用程序通常使用用户名和密码来验证用户身份。如果传递了有效的用户名和密码,应用程序可以验证您是您声称的用户,因为密码应该只有您和应用程序知道。

注意:使用用户名/密码身份验证的 Web 应用程序很少将密码以明文形式存储在数据库中。相反,他们使用一种称为哈希的技术来存储密码的哈希值。这允许后端在不知道密码的情况下验证密码。

哈希函数是一个数学函数,它接受任意输入,并且对于相同的输入始终生成相同的固定长度的字符串/数字。哈希函数的强大之处在于您可以从密码转到哈希值,但不能从哈希值转到密码。

这允许在不存储实际密码的情况下验证用户提交的密码。在数据库访问泄露的情况下,存储密码哈希值可以保护用户,因为无法使用哈希密码登录。

近年来,鉴于大量重要网站遭到破坏,Web 安全已成为越来越受关注的问题。这种趋势通过引入更安全的身份验证方法(例如多因素身份验证)影响了安全处理方式。

多因素身份验证是一种身份验证方法,用户在成功出示两个或多个证据(也称为因素)后才会被验证身份。例如,当从 ATM 取款时,需要两个身份验证因素:拥有银行卡和 PIN 码。

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

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

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

将身份验证外包给用户的电子邮件帐户意味着应用程序将继承用户电子邮件帐户安全性的优点和缺点。但是现在,大多数电子邮件服务都提供了第二因素身份验证和其他安全措施的选项。

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

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

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

身份验证流程如下所示

  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 编码:标头有效负载签名,如下所示(各部分用 . 分隔)

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

如果您使用 Base64 解码上面的标头和有效负载,您将得到以下内容

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

令牌的签名部分是通过将标头有效负载密钥传递到签名算法(在本例中为 HS256)来创建的。密钥仅后端知道,用于验证令牌的真实性。

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

注意:这种方法称为有状态 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 中的身份验证基于方案策略的概念。方案是一种处理身份验证的方式,而策略是方案的预配置实例。在本文中,您只需要根据 jwt 身份验证方案定义策略。

您将把所有这些逻辑封装在一个 auth 插件中。

添加依赖项

首先,将以下依赖项添加到您的项目中

创建 auth 插件

接下来,您将创建一个 auth 插件来封装身份验证逻辑。

src/plugins/ 文件夹中创建一个名为 auth.ts 的新文件

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

注意:auth 插件定义了对 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":"[email protected]"}' localhost:3000/login。您应该看到从后端记录的令牌:email token for [email protected]: 27948216

定义身份验证端点

此时,后端可以创建用户、生成电子邮件令牌并通过电子邮件发送它们。但是,生成的令牌仍然无法正常工作。您现在将通过创建 /authenticate 端点来实现身份验证的第二步,验证数据库中的电子邮件令牌,并在 authorization 标头中返回用户长期有效的 JWT 身份验证令牌。

首先,将以下路由声明添加到 authPlugin

该路由需要电子邮件和 emailToken。由于只有尝试登录的合法用户才会知道这两者,因此猜测电子邮件和 emailToken 都变得更加困难,从而降低了暴力破解攻击猜测八位数字的风险。

接下来,将以下内容添加到 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":"[email protected]"}' localhost:3000/login 您应该看到从后端记录的令牌:email token for [email protected]: 13080740
  3. 获取该令牌并使用 curl 调用 /authenticate 端点:curl -v --header "Content-Type: application/json" --request POST --data '{"email":"[email protected]", "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)是基于角色的授权的一种形式,其中用户具有“管理员”角色。

端点的授权规则

为了实施建议的授权规则,我们将首先重新访问端点列表以及建议的授权规则

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 还提供了一种使用作用域声明性地实现基于角色的身份验证的方法。但是,建议的基于资源的授权方法——用户的权限取决于请求的特定资源——需要更精细的控制,而作用域无法实现,因此使用了 pre 函数。

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

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

pre 函数现在将在 getUserHandler 之前被调用,并且仅授权管理员或请求他们自己 userId 的用户访问。

注意: 在上一部分中,您已定义默认的身份验证策略,因此严格来说不需要定义 options.auth。但为每个路由显式定义身份验证要求是一个好的做法。

检查点: 要验证授权逻辑是否已正确实现,您将创建一个测试用户和测试管理员,并调用 /users/{userId} 端点

  1. 使用 npm run dev 启动服务器
  2. 运行 seed-users 脚本以创建测试用户和测试管理员:npm run seed-users。您应该获得类似于以下内容的结果
  1. 通过调用 POST /login 端点登录为 [email protected],如下所示
  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 pre 函数正确授权用户访问他们自己的用户个人资料。要测试管理员功能,请从第三步开始重复,但使用电子邮件 [email protected] 以测试管理员身份登录。管理员应该能够获取两个用户个人资料。

将授权 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 手册

为了使测试通过,您将直接在测试中使用 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 身份验证方案来定义身份验证策略。在身份验证策略的验证函数中,您对照数据库检查令牌,并使用与授权规则相关的信息填充 credentials 对象。

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

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

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

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

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

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

虽然 Prisma 旨在使关系数据库的使用变得容易,但了解底层数据库和身份验证原则仍然很有用。

如果您有任何疑问,请随时通过 Twitter 与我联系。

不要错过下一篇文章!

注册 Prisma 新闻通讯