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将数据保存到数据库的种子脚本。

在本系列的第二篇文章中,你基于第一篇文章中的数据模型和 REST API 构建了一个 Prisma 模式。你使用 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 封电子邮件。

注册 signup 后,请在 SendGrid 控制台中前往 API 密钥,生成一个 API 密钥并妥善保管。

克隆存储库

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

首先,克隆存储库并安装依赖项:

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

启动 PostgreSQL

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

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

身份验证和授权概念

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

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

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

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

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

Web 应用程序中的身份验证

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

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

哈希函数是一个数学函数,它接受任意输入,并且在给定相同输入的情况下总是生成相同的固定长度字符串/数字。哈希函数的强大之处在于你可以从密码生成哈希值,但不能从哈希值生成密码。

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

近年来,鉴于许多重要网站遭到泄露,网络安全已成为日益关注的问题。这一趋势影响了安全处理方式,引入了更安全的身份验证方法,例如多因素身份验证

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

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

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

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

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

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

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

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

身份验证流程如下

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

有两种令牌类型

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

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

JSON Web 令牌

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

JWT 令牌包含三个部分,它们经过 Base64 编码:headerpayloadsignature,结构如下(各部分用 . 分隔)

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

如果你使用 Base64 解码上面的 header 和 payload,你将得到以下内容

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

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

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

注意: 这种方法称为有状态 JWT,其中令牌引用存储在数据库中的会话。虽然这意味着验证请求需要对数据库进行往返,这增加了处理请求所需的时间,但这种方法更安全,因为令牌可以由后端撤销。

向 Prisma 模式添加令牌模型

你需要将令牌存储在数据库中,以便在发出请求时可以对其进行验证。在此步骤中,你将向 Prisma 模式添加一个新的 Token 模型,并更新 User 模型以使某些字段可选。

打开位于 prisma/schema.prisma 的 Prisma 模式并按如下方式更新

让我们回顾一下引入的更改

  • 启用 connectOrCreatetransactionApi 预览功能。这些功能将在接下来的步骤中使用。
  • 删除 aggregateApi 预览功能,该功能自 Prisma 2.5.0 起已稳定。
  • User 模型中,firstNamelastName 现在是可选的。这允许用户仅使用电子邮件登录/注册。
  • 添加了一个新的 Token 模型。每个用户可以拥有多个令牌,从而使关系成为 1-n。 Token 模型包含相关字段,用于适应过期时间、两种令牌类型(带 TokenType 枚举)以及电子邮件令牌的存储。

要迁移数据库模式,请按如下方式创建并运行迁移

检查点: 你应该在输出中看到类似以下内容

注意: 运行 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 插件中。

添加依赖

首先将以下依赖项添加到你的项目

创建认证插件

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

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

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

注意: auth 插件定义了对 prismahapi-auth-jwt2app/email 插件的依赖。prisma 插件在本系列的第 2 部分中定义,将用于访问 Prisma Client。hapi-auth-jwt2 插件定义了 jwt 身份验证方案,你将使用它来定义身份验证策略。最后,app/email 将确保你可以访问 sendEmailToken 函数。

定义登录端点

authPluginregister 函数中,定义一个新的登录路由,如下所示

注意: options.auth 设置为 false,以便在你设置默认身份验证策略后,该端点将保持开放状态,该策略将默认要求所有未明确禁用身份验证的路由进行身份验证。

在插件的注册函数之外,添加以下内容

loginHandler 执行以下操作

  • 电子邮件从请求 payload 中获取
  • 生成令牌并将其保存到数据库中
  • 使用 connectOrCreate,如果 payload 中电子邮件地址的用户不存在,则创建该用户。否则,将创建与现有用户的关系。
  • 令牌被发送到 payload 中的电子邮件地址(如果未设置 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 一起传递到凭据对象。

大多数端点需要身份验证(因为默认的身份验证策略),但仍然没有授权规则。这意味着要访问 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-function 并将其分配给这两个端点。

向用户端点添加授权

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

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

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

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

预函数现在将在 getUserHandler 之前调用,并且只授权管理员或请求其自身 userId 的用户访问。

注意: 在上一部分中,你已经定义了默认的身份验证策略,因此不严格要求定义 options.auth。但是,明确定义每个路由的身份验证要求是一个好习惯。

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

  1. 使用 npm run dev 启动服务器
  2. 运行 seed-users 脚本以创建测试用户和测试管理员:npm run seed-users。你应该会得到类似以下结果
  1. test@prisma.io 身份登录,通过以下方式调用 POST /login 端点
  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 预函数将正确授权用户访问其自己的用户配置文件。要测试管理员功能,请从第三步开始重复,但以电子邮件 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 发送电子邮件的能力。

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

你还执行了数据库迁移,并引入了一个新的 Token 表,它与 User 表通过 Prisma Migrate 建立了 n-1 关系。

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

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

本文涵盖了所有端点子集的授权。接下来,你可以做以下事情

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

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

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

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

不要错过下一篇文章!

订阅 Prisma 新闻通讯

© . This site is unofficial and not affiliated with Prisma Data, Inc.