欢迎阅读本系列的第二篇文章,您将学习如何使用 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
prop 渲染在 <div>
内部。要了解它在实际使用时如何渲染,请查看下面的代码片段:
创建登录表单
现在,您可以将该组件导入到 app/routes/login.tsx
文件中,并将您的 <h2>
标签包装在新 Layout
组件内,而不是它目前所在的 <div>
中
构建表单
接下来添加一个登录表单,它包含 email
和 password
输入框,并显示一个提交按钮。在顶部添加一个友好的欢迎信息,以便用户进入您的网站时欢迎他们,并使用 Tailwind 的 flex 类将整个表单居中显示在屏幕上。
此时,您无需担心 <form>
的 action 指向何处,只需确保其 method
值为 "post"
即可。稍后您将看到 Remix 为我们设置 action 的一些酷炫魔法!
创建表单字段组件
随着您添加更多表单,输入字段及其标签将在整个应用程序中进行大量重写,因此将它们拆分成一个名为 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 模式中尚不存在(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 选项的信息这里。
您还需要在 .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
函数时,会自动将 POST
请求配置到 /login
路由。
如果您尝试登录或创建帐户,您应该会看到之后被发送到主屏幕。成功!🎉
在私有路由上授权用户
为了提升用户体验,您接下来要做的就是根据用户是否拥有有效会话,自动将其重定向到主页或登录页面。
在 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
Hook 来访问从 action 返回的数据,以便提取错误消息。
此代码添加了以下内容:
- 捕获从
action
函数返回的数据。 - 设置一个
errors
变量,该变量将以对象形式保存特定于字段的错误,例如“无效电子邮件”。它还设置了一个formError
变量,该变量将保存用于显示表单消息的错误消息,例如“登录不正确”。 - 更新
formData
状态变量,如果可用,则默认使用action
函数返回的任何值。
如果用户看到错误并切换表单,您需要清除表单和所有显示的错误。使用这些 effects
来实现此目的:
有了这些,您终于可以让您的表单和字段知道要显示哪些错误了。
现在,您应该会在注册和登录表单上看到错误消息和表单重置功能正常工作!
总结与展望
感谢您一直坚持到本节结束,(😉)赞!虽然内容很多,但希望您能理解以下内容:
- 如何在 Remix 中设置路由。
- 如何构建带验证的登录和注册表单。
- 基于会话的身份验证如何工作。
- 如何通过实现授权来保护私有路由。
- 在创建和验证用户时,如何使用 Prisma 存储和查询您的数据。
在本系列的下一部分中,您将构建 Kudos 的主页和点赞分享功能。您还将为点赞动态添加搜索和过滤功能。
不要错过下一篇文章!
订阅 Prisma 新闻通讯