欢迎阅读本系列的第二篇文章,您将学习如何使用 MongoDB、Prisma 和 Remix 从头开始构建一个全栈应用程序!在本部分中,您将为您的 Remix 应用程序设置基于会话的身份验证。
目录
- 简介
- 设置登录路由
- 创建一个可复用的布局组件
- 创建注册表单
- 添加注册表单
- 身份验证流程
- 构建注册功能
- 构建登录功能
- 添加会话管理
- 处理登录和注册表单提交
- 在私有路由上授权用户
- 添加表单验证
- 总结和下一步
简介
在本系列的上一部分中,您设置了 Remix 项目并启动并运行了 MongoDB 数据库。您还配置了 TailwindCSS 和 Prisma,并开始在 schema.prisma 文件中建模 User 集合。
在本部分中,您将在应用程序中实现身份验证,允许用户通过登录和注册表单创建帐户并登录。
注意:此项目的起点可在 GitHub 存储库的 part-1 分支中找到。如果您想查看此部分的最终结果,请转到 part-2 分支。
开发环境
为了遵循所提供的示例,您需要...
- ... 已安装 Node.js。
- ...已安装Git。
- ...已安装TailwindCSS VSCode 扩展。(可选)
- ...已安装Prisma VSCode 扩展。(可选)
注意:可选扩展为 Tailwind 和 Prisma 增加了一些非常好的智能感知和语法高亮。
设置登录路由
您需要做的第一件事是设置一个 /login 路由,您的登录和注册表单将位于此处。
要在 Remix 框架中创建路由,请将文件添加到 app/routes 文件夹。该文件的名称将用作路由的名称。有关 Remix 中路由工作方式的更多信息,请查看其文档。
在 app/routes 中创建一个名为 login.tsx 的新文件,其内容如下:
路由文件的默认导出是 Remix 渲染到浏览器中的组件。
使用 npm run dev 启动开发服务器并导航到 https://:3000/login,您应该会看到路由已渲染。

这有效,但看起来还不太好……接下来,您将通过添加实际的登录表单来美化它。
创建一个可复用的布局组件
首先,创建一个组件,您将把路由包装在其中以提供一些共享的格式和样式。您将使用组合模式来创建此 Layout 组件。
要查看实际效果,请在 app 文件夹中创建一个名为 components 的新文件夹。在该文件夹中创建一个名为 layout.tsx 的新文件。
在该文件中,导出以下函数组件
此组件使用 Tailwind 类来指定您希望包装在组件中的任何内容都占用屏幕的完整宽度和高度,使用等宽字体,并显示适度深蓝色作为背景。
注意 children 属性渲染在 <div> 内部。要了解它在使用时将如何渲染,请查看下面的代码片段
创建注册表单
现在,您可以将该组件导入到 app/routes/login.tsx 文件中,并将 <h2> 标签包装在新的 Layout 组件中,而不是它当前所在的 <div> 中
构建表单
接下来,添加一个登录表单,它接受 email 和 password 输入,并显示一个提交按钮。在顶部添加一条友好的欢迎信息,在用户进入您的网站时问候他们,并使用 Tailwind 的 flex 类将整个表单居中。

此时,您不需要担心 <form> 的动作指向何处,只需它具有 method 值 "post" 即可。稍后您将查看一些很酷的 Remix 魔法,它为我们设置了动作!
创建一个表单字段组件
在整个应用程序中添加更多表单时,输入字段及其标签将被大量重写,因此将它们分解为名为 FormField 的受控组件,以避免代码重复。
在 app/components 中创建一个名为 form-field.tsx 的新文件,您将在其中构建 FormField 组件。然后添加以下代码以开始使用
这将定义并导出与您之前在登录表单中相同的标签和输入组合,只是此组件将具有可配置选项
htmlFor:用于输入字段的id和name属性以及标签的htmlFor属性的值。label:要在标签中显示的文本。value:输入字段当前的受控值。type:可选 允许您设置输入字段的type属性,但默认为'text'。onChange:可选 允许您提供一个函数,当输入字段的值更改时运行。默认为空函数调用。
您现在可以用此组件替换现有标签和输入
这将导入新的 FormField 组件,其状态将由父级(在本例中为登录表单)管理。值的任何更新都将使用 handleInputChange 函数进行跟踪。
您稍后将回到 FormField 组件以添加错误消息处理,但目前它将完成其所需的工作!
添加注册表单
您还需要一种让用户注册帐户的方式,这意味着您还需要另一个表单。此表单将接收四个值:
电子邮件密码名字姓氏
为了避免创建与 /login 路由几乎完全相同的 /signup 路由,请重新利用登录表单,使其可以在两个不同的操作之间切换:登录和注册。
将表单操作存储在状态中
首先,您需要一种方法让用户能够在表单之间切换,并让您的代码能够区分这些表单。
在 Login 组件的顶部,在状态中创建另一个变量来保存您的 action。
注意:默认状态将是登录屏幕。
接下来,您需要一种方法来切换要查看的状态。在“欢迎使用 Kudos”消息上方,添加以下按钮
此按钮的文本将根据 action 状态而变化。 onClick 方法将状态在 'login' 和 'register' 值之间来回切换。
此页面上有一些静态文本,您可能希望根据您正在查看的表单进行调整。特别是“登录以赞美!”副标题和表单内的“登录”按钮。
更改表单的副标题,以便在每个表单上显示不同的消息
完全删除登录按钮,并将其替换为以下 <button>
这个新按钮具有 name 和 value 属性。该值设置为状态的 action。当您的表单提交时,此值将作为 _action 与表单数据一起传递。
注意:此技巧仅适用于
<button>,如果name属性以下划线开头。
根据您选择的表单,您现在应该会看到更新的消息。尝试点击“注册”和“登录”按钮几次。

添加可切换字段
此页面上的文本看起来很棒,但两个表单都显示相同的输入字段。您最后需要做的是在显示注册表单时添加更多字段。
在 password 字段后添加以下字段,并确保将新字段添加到 formData 对象。
这里做了两个更改
- 您在
formData状态中添加了两个新键。 - 您添加了两个字段,这些字段根据您正在查看的是登录表单还是注册表单进行条件渲染。
您的登录和注册表单现在在视觉上已完成!是时候进入下一部分了:使表单功能化。

身份验证流程
本节是激动人心的部分,您将使您设计和构建的一切真正发挥作用!
然而,在继续之前,您需要在项目中添加一个新的依赖项。运行以下命令:
这会安装 bcryptjs 库及其类型定义。您稍后将使用它来哈希和比较密码。
身份验证将基于会话,遵循 Remix Jokes App 教程的身份验证中使用的相同模式。
为了更好地可视化您的应用程序身份验证流程,请查看下图。

将有一系列步骤来验证用户身份,其中有两个潜在的途径(登录和注册)
- 用户将尝试登录或注册。
- 表单将被验证。
- 将调用
login或register函数。 - 如果正在登录,服务器端代码将确保存在具有所提供登录详细信息的用户。如果正在注册帐户,它将确保不存在具有所提供电子邮件的帐户。
- 如果上述步骤通过,将创建一个新的 cookie 会话,用户将被重定向到主页。
- 如果某个步骤未通过并出现问题,用户将被送回登录或注册屏幕,并显示错误。
要开始此操作,请在 app 目录中创建一个名为 utils 的文件夹。您将在此处存储任何帮助程序、服务和配置文件。
在该新文件夹中,创建一个名为 auth.server.ts 的文件,您将在其中编写身份验证和会话相关方法。
注意:Remix 不会将带有
.server前缀的文件与发送到浏览器的代码捆绑在一起。
构建注册功能
您将构建的第一个函数是注册函数,它将允许用户创建新帐户。
从 app/utils/auth.server.ts 导出一个名为 register 的异步函数
在 app/utils 内的另一个新文件中,名为 types.server.ts,创建并导出一个定义注册表单将提供的字段的 type。
将该 type 导入到 app/utils/auth.server.ts 中,并在 register 函数中使用它来描述一个 user 参数,该参数将包含注册表单的数据
当调用此 register 函数并提供 user 时,您需要检查的第一件事是是否已存在具有所提供电子邮件的用户。
注意:请记住,
创建 PrismaClient 实例
您将使用 PrismaClient 执行数据库查询,但您的应用程序尚无其可用实例。
在 app/utils 文件夹中创建一个名为 prisma.server.ts 的新文件,您将在其中创建并导出 Prisma Client 的实例。
注意:上面已采取预防措施,以防止在开发过程中实时重新加载使您的数据库连接饱和。
您现在有了一种访问数据库的方法。在 app/utils/auth.server.ts 中,导入实例化的 PrismaClient,并将以下内容添加到 register 函数中
注册函数现在将查询数据库中是否存在任何具有所提供电子邮件的用户。
此处使用了 count 函数,因为它返回一个数值。如果没有匹配查询的记录,它将返回 0,这被评估为 false。否则,将返回大于 0 的值,这被评估为 true。
如果找到用户,该函数将返回一个状态码为 400 的 json 响应。
更新您的数据模型
现在您可以确保当用户尝试注册时,不会存在具有所提供电子邮件的另一个用户。接下来,register 函数应创建一个新用户。但是,我们将存储的几个字段在 Prisma schema 中尚不存在(firstName 和 lastName)。
您将把这些数据存储在 User 模型中名为 profile 的字段中,该字段包含一个嵌入式文档。
打开您的 prisma/schema.prisma 文件并添加以下 type 块
type 关键字用于定义复合类型——允许您在文档中定义文档。使用复合类型而非 JSON 类型的好处是,在查询文档时可以获得类型安全。
这非常有用,因为它赋予您明确定义数据形状的能力,否则由于 MongoDB 的灵活性,这些数据本可以非常灵活,并且可以包含任何内容。
您尚未将此新复合类型(嵌入式文档的另一个名称)用于描述字段。在您的 User 模型中,添加一个名为 profile 的新字段,并使用 Profile 类型作为其数据类型
太棒了,您的 User 模型现在将包含一个 profile 嵌入式文档。重新生成 Prisma Client 以考虑这些新更改:
注意:您不需要运行
prisma db push,因为您没有添加任何新的集合或索引。
添加用户服务
在 app/utils 中创建另一个文件,名为 user.server.ts,其中将编写任何用户特定的函数。在该文件中添加以下函数和导入
这个 createUser 函数做了几件事
- 它对注册表单中提供的密码进行哈希处理,因为您不应将其明文存储。
- 它使用 Prisma 存储新的
User文档。 - 它返回新用户的
id和email。
注意:您可以通过传入 JSON 对象直接在此查询中填写
profile嵌入文档的详细信息,并且由于 Prisma 生成的类型,您将看到一些不错的自动补全。

此函数将用于您的 register 函数,以处理用户的实际创建。在 app/utils/auth.server.ts 中,导入新的 createUser 函数并在 register 函数中调用它。
现在,当用户注册时,如果不存在具有所提供电子邮件的另一个用户,将创建一个新用户。如果在用户创建过程中出现问题,将向客户端返回错误以及传入的 email 和 password 值。
构建登录功能
login 函数将接收 email 和 password,因此要开始此函数,请在 app/utils/types.server.ts 中创建一个新的 LoginForm 类型来描述该数据
然后通过将以下内容添加到 app/utils/auth.server.ts 来创建 login 函数
上面的代码...
- ...导入了新的
type和bcryptjs库。 - ...查询具有匹配电子邮件的用户。
- ...如果找不到用户或提供的密码与数据库中的哈希值不匹配,则返回
null值。 - ...如果一切顺利,则返回用户的
id和email。
这将确保提供了正确的凭据,并为您提供创建新 cookie 会话所需的数据。
添加会话管理
您现在需要一种方法来在用户登录或注册帐户时为其生成 cookie 会话。Remix 提供了使用其 createCookieSessionStorage 函数存储这些 cookie 会话的简便方法。
将该函数导入到 app/utils/auth.server.ts 中,并在导入后直接添加一个新的 cookie 会话存储
上面的代码创建了一个会话存储,并设置了一些配置:
name:Cookie 的名称。secure:如果为true,则只允许通过 HTTPS 发送 cookie。secrets:会话的秘密。sameSite:指定是否可以通过跨站点请求发送 cookie。path:URL 中必须存在的路径才能发送 cookie。maxAge:定义 cookie 在自动删除之前允许存活的时间段。httpOnly:如果为true,则不允许 JavaScript 访问 cookie。
注意:在此处了解有关不同 cookie 选项的更多信息:here。
您还需要在 .env 文件中设置会话密钥。添加一个名为 SESSION_SECRET 的变量,并为其指定一个密钥值。例如:
会话存储已设置完毕。在 app/utils/auth.server.ts 中再创建一个函数,用于实际创建 cookie 会话
此函数...
- ...创建一个新会话。
- ...将该会话的
userId设置为登录用户的id。 - ...在调用此函数时,将用户重定向到您可以指定的路由。
- ...在设置 cookie 标头时提交会话。
createUserSession 函数现在可以在用户成功注册或登录时在 register 和 login 函数中使用。
处理登录和注册表单提交
您已经创建了创建新用户和登录所需的所有功能。现在,您将在您构建的表单中运用这些功能。
在 app/routes/login.tsx 中,导出 action 函数。
注意:Remix 会查找名为
action的导出函数,以在您定义的路由上设置 POST 请求。
现在,在 app/utils 内的一个新文件中,名为 validators.server.ts,创建几个验证器函数,这些函数将用于验证表单输入。
在 app/routes/login.tsx 中的 action 函数中,从请求中获取表单数据并验证其格式是否正确。
上面的代码看起来可能有点吓人,但简单来说,它……
- ...从请求对象中提取表单数据。
- ...确保提供了
email和password。 - ...如果
_action值为"register",则确保提供了firstName和lastName。 - ...如果发生任何问题,则返回错误以及表单字段值,以便您可以稍后在这些字段无效时使用用户的输入和错误消息重新填充表单。
您最后需要做的是,如果输入看起来不错,则实际运行您的 register 和 login 函数。
switch 语句将允许您根据表单中 _action 值的内容有条件地运行 login 和 register 函数。
为了实际触发此操作,表单需要发布到此路由。幸运的是,Remix 会处理这个问题,因为它会在识别到导出的 action 函数时,自动为 /login 路由配置 POST 请求。
如果您尝试登录或创建帐户,您应该会看到之后被发送到主屏幕。成功!🎉

在私有路由上授权用户
接下来要做的事情是自动将用户重定向到主页或登录页面,具体取决于他们是否拥有有效的会话,从而大大改善用户体验。
在 app/utils/auth.server.ts 中,您需要添加几个辅助函数。
这是许多新功能。以下是上述函数将执行的操作
requireUserId检查用户的会话。如果存在,则成功并仅返回userId。但是,如果失败,它会将用户重定向到登录屏幕。getUserSession根据请求的 cookie 获取当前用户的会话。getUserId从会话存储中返回当前用户的id。getUser返回与当前会话关联的整个user文档。如果未找到,则用户将被注销。logout销毁当前会话并将用户重定向到登录屏幕。
有了这些,您就可以在私有路由上实现一些不错的授权。
在 app/routes/index.tsx 中,如果用户未登录,则通过添加以下内容将用户返回到登录屏幕:
注意:Remix 在提供页面之前运行
loader函数。这意味着加载器中的任何重定向都会在您的页面可以提供之前触发。
如果您尝试在未登录的情况下导航到应用程序的根路由(/),您应该会重定向到登录屏幕,并在 URL 中带有 redirectTo 参数。
注意:如果您已经登录,可能需要清除您的 cookie。
接下来,做恰好相反的事情。如果已登录用户尝试访问登录页面,他们应该被重定向到主页,因为他们已登录。将以下代码添加到 app/routes/login.tsx
添加表单验证
太棒了!您的登录和注册表单正在运行,并且您已在您的私有路由上设置了授权和重定向。您几乎到达终点线了!
最后要做的是添加表单验证并显示从 action 函数返回的错误消息。
更新 FormField 组件,使其能够处理错误消息。
此组件现在将接收一条错误消息。当用户开始在该字段中键入时,如果显示了任何错误消息,它将被清除。
在登录表单中,您需要使用 Remix 的 useActionData 钩子访问从操作返回的数据,以便提取错误消息。
此代码添加了以下内容:
- 挂接到从
action函数返回的数据。 - 设置一个
errors变量,该变量将以对象形式保存字段特定的错误(例如“无效电子邮件”)。它还设置一个formError变量,该变量将保存用于显示表单消息(例如“登录错误”)的错误消息。 - 更新
formData状态变量,使其默认为action函数返回的任何可用值。
如果用户看到错误并切换表单,您需要清除表单和所有显示的错误。使用这些 effects 来实现此目的:
有了这些,您终于可以告知您的表单和字段要显示哪些错误。
现在您应该会看到错误消息和表单重置在您的注册和登录表单上正常工作!

总结和下一步
祝贺 (😉) 您坚持到本节的最后!涵盖的内容很多,但希望您能从中了解以下内容:
- 如何在 Remix 中设置路由。
- 如何构建带有验证的登录和注册表单。
- 基于会话的身份验证的工作原理。
- 如何通过实施授权来保护私有路由。
- 如何在创建和验证用户时使用 Prisma 存储和查询数据。
在本系列的下一部分中,您将构建 Kudos 的主页和 Kudos 共享功能。您还将为 Kudos feed 添加搜索和过滤功能。
不要错过下一篇文章!
订阅 Prisma 新闻通讯