用Next.js动态路由实现重设密码功能
从一般意义上讲,当很多人试图开始使用某个框架时,认证本身就是一个障碍,而Next.js也不例外。
虽然,在用Next.js构建认证的应用程序方面有很多资源。好吧,甚至有一个开源项目,从字面上看是在处理认证。
但是,本文的范围并不围绕整个认证的概念。我们只选取认证过程中的一个特定模式:"重置密码 "流程,以及如何在Web应用程序的客户端--前端实现它。
在这篇文章中,你将看到如何通过使用常见的数据获取工具Axios--Next.js的内置动态路由功能和useRouter 钩子来实现这一功能。
重置密码流程概述
自网络出现以来,工程师们一直在努力为网络早期出现的问题提供解决方案,网络上的软件安全也不例外。
有这样一个流行的说法,即"用户总是会忘记他们的密码",这就是绝对的事实。很多人甚至害怕 "重置密码 "的页面,因为仔细想想,在花了很多时间试图猜测他们的密码之后--所有这些都无济于事--当他们登陆这个特殊的页面时,他们不是感到沮丧就是感到愤怒。
在我们创建用户界面的时候,我们也应该尽可能地让用户有一个愉快的体验。尽管我们很想直接搞定重置密码的流程,但这个流程的用户体验也应该被优先考虑。
密码重置过程的常见流程如下:
- 用户在尝试签名而不成功后感到很沮丧。他们点击了 "密码重置 "链接,然后被重定向到相应的页面。他们看到的用户界面是典型的网络表格,会输入他们的电子邮件地址或用户名。
- 当他们在输入框中输入他们的电子邮件地址或用户名时,他们点击按钮,上面有常见的 "给我发一个恢复链接 "的文字。
- 他们会得到一个确认,即一个安全链接已被发送到他们的电子邮件。有时,这个确认文本可以显示在一个类似卡片的组件中,或者一个随着时间推移逐渐消失的模式。
注意:为了安全和良好的用户体验,使用与此相当类似的文本是不错的。"一封电子邮件已被发送到您的收件箱。当你收到时,请点击链接"。你可以以任何你认为合适的方式构建这句话,只要它不透露他们输入的电子邮件或用户名存在于数据库中。这种方法可以防止攻击者知道该电子邮件是否存在,从而破坏他们可能想用上述电子邮件地址进行的任何网络钓鱼尝试。至于用户体验,文本并不能向用户保证他们所输入的凭证是正确的。这反过来又使他们能够仔细检查他们所提交的任何凭证。
- 发送到他们电子邮件地址的链接包含一个JWT和他们的
user_id,或者在这种情况下,他们的电子邮件地址。 - 点击该链接后,他们会被重定向到他们可以输入新密码的路线/页面。用户所处的路线可能是下面的形式
https://localhost:3000/reset-password/user-email/JWToken
- 流程的最后一部分是验证生成的JWT是否与用户的账户相关。如果不是,我们通过渲染从后端获得的错误信息来抛出一个错误。
现在你已经看到了 "重置密码 "流程的结构,让我们看看如何用Next.js实现它。
了解动态路由
在本节中,我们将通过Next.js项目的文件夹结构来说明动态路由的概念,并看看我们将如何把它整合到 "重置密码 "功能中。但首先,让我们建立一个Next.js应用程序。
npx create-next-app app-name
上面的命令为我们做到了这一点。Next.js团队已经对该框架进行了新的更新,他们还在默认项目结构中引入了一些新的文件夹和功能。然而,我们不会在这方面多做介绍,因为这不在本文的范围之内。如果你想的话,你可以在这里阅读更多关于更新的内容。
在下面的片段中,你会看到我们将在本文中与之互动的文件的基本结构。
└── pages /
├── forgot-password/
│ └── [token]/
│ └── [email].js
├── _app.js
└── index.js
在上面,你会看到文件夹结构中的文件都相当小。这是因为我想在这篇文章中尽可能的简洁。
而且,由于 "密码重置 "流程的实现是我们最关心的问题,我认为最好是少一点杂乱。现在,让我们对这个结构有一点了解。
你会注意到,我们在pages 目录下有一个forgot-password 文件夹,其中包含一些文件。但这些文件的命名方式与其他文件的命名方式有很大不同。文件的名称--token和email.js--是用一对方括号包裹的。
像这样命名的文件夹和文件被称为动态路由,由于它们在pages 目录中,它们自动成为可以被浏览器访问的路由。它们是动态的,因为这些路由的值不是静态的,这意味着它们会随着时间的推移而改变。
当你决定建立一个博客时,或者当你与根据登录到一个应用程序的用户类型而变化的数据进行交互时,这种命名文件的模式通常会在行动中看到。你可以看看我在建立博客时是如何利用Next.js的这个功能的。你也可以在Next.js文档中了解更多信息。
在forgot-password 文件夹中,这里可以访问包含忘记密码表单的用户界面的路径。看看下面的内容吧。
http://localhost:3000/forgot-password/token/email
由于它是一个动态路由,token 和email URL参数将始终根据试图重置密码的用户而改变。用户A的令牌和电子邮件将与用户B的不同。
跳转后更多在下面继续阅读 ↓
用Userouter钩子读取Url参数
Next.js中的useRouter 钩子可以用来实现很多实用的前端UI实现--从用.pathname 键实现一个活动导航条项目的普通想法,到更复杂的功能。
现在让我们看看如何用useRouter 钩子从动态路由读取URL参数,好吗?要做到这一点,你必须先将该模块导入你的页面/组件中。
import { useRouter } from 'next/router'
export default function PageComponent({ children }) {
const router = useRouter()
return (
<React.Fragment>
{/* page content falls below */}
<div>{children}</div>
</React.Fragment>
)
}
上面的片段显示了该钩子的基本用法。由于我们对URL的查询参数感兴趣,我们最好对钩子的query 方法进行重构,而不是像这样做:router.query 。我们就在下面做一些类似的事情。
import { useRouter } from 'next/router'
const { query } = useRouter()
我们现在可以继续创建变量来存储我们想要的URL参数。下面的片段显示了你如何做到这一点。
const token = query.token
const email = query.email
注意,query.token 和query.email 值是文件名的结果。回顾一下forgot-password 文件夹中的文件夹结构,我们有[email].js 和[token] 文件。如果你把这些文件分别改名为[userEmail].js 和[userToken] ,分配这些变量的模式就会变成下面这样的情况。
const token = query.userToken
const email = query.userEmail
你可以随时把这些变量记录到控制台,看看结果。
现在你已经了解了这些参数是如何从URL中获得的,让我们开始构建表单的结构。
构建表单
在本节中,我们将介绍构建表单的过程,以及如何使用Axios通过任意的API端点进行数据获取。我们不会专注于这些表单的样式和结构的解释。我假设你已经知道如何结构和样式一个基本的React表单。因此,让我们开始在忘记密码路线上的表单布局。
import React from 'react'
import axios from 'axios'
import { ErrModal, SuccessModal } from '../components/Modals'
export const DefaultResetPassword = () => {
const [email, setEmail] = React.useState('')
const [loading, setLoading] = React.useState(false)
const handleForgot = () => { } // we’ll see this later
return (
<div>
<form onSubmit={handleForgot} className="reset-password">
<h1>Forgot Password</h1>
<p>You are not alone. We’ve all been here at some point.</p>
<div>
<label htmlFor="email">Email address</label>
<input
type="email"
name="email"
id="email"
placeholder= “your email address”
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<button name="reset-pwd-button" className="reset-pwd">
{!loading ? ‘Get secure link’: ‘Sending...’}
</button>
</form>
</div>
)
}
上面的片段显示了当你进入忘记密码路线时,你会看到的UI的基本结构。你会注意到粗体 "忘记密码 "文本下面的段落标签中的文本。
<p>You are not alone. We’ve all been here at some point</p>
有了上面这种类型的文字,你正在改善那些进入你的应用程序的忘记密码页面的人的用户体验。你在向他们保证,他们忘记密码并不是什么大事,所以没有必要为此感到难过。
你不一定需要使用上面的确切文本。你只需确保你使用的任何文字都有一种同情的语气。
现在,让我们进入这个表单的重要部分,也就是我们需要声明一个函数,将用户在输入栏中输入的电子邮件发送到后台。
import { authEndpoints } from '../endpoints'
export const DefaultResetPassword = () => {
const handleForgot = async (e) => {
e.preventDefault()
try {
setLoading(true)
const response = await axios({
method: 'POST',
url: authEndpoints.recover,
data: {
email,
},
headers: {
'Content-Type': 'application/json',
},
})
setResestSuccess(response.data.msg)
setLoading(false)
setResetError('')
} catch (error) {
setLoading(false)
const { data } = error.response
setResetError(data.msg)
setResestSuccess(null)
}
}
return <div>{/* ...previous form component */}</div>
}
从上面的片段中,你会注意到我们正在导入我们要发送POST请求的API端点--这就是为什么我们要把它作为一个变量传给Axios方法中的url key。
POST请求接收用户的电子邮件地址作为有效载荷,这又将在后端进行验证,并为该电子邮件地址生成一个JWT,用于授权用户的密码重置过程。
setResestSuccess(response.data.msg)
setLoading(false)
setResetError('')
catch (error) {
setLoading(false)
const { data } = error.response
setResetError(data.msg)
setResestSuccess(null)
}
当你看一下上面的片段,你会发现我们使用了一些已经声明的状态变量的回调函数。
一个例子是setLoading 函数,它的值在try 块中被设置为true 。然后,当数据被成功发送时,它的值被设置为false。而如果没有,我们有一个catch 块,它将 "捕捉 "错误并显示我们从端点解构的错误信息。
你还会注意到,在上面的片段中,有几个状态回调函数,如setResestSuccess 和setResetError 。
这些设置器是从状态变量的声明中获得的。请看下面的内容。
import React from 'react'
import { ErrModal, SuccessModal } from '../components/Modals'
export const DefaultResetPassword = () => {
const [resetSuccess, setResestSuccess] = React.useState()
const [resetError, setResetError] = React.useState()
return (
<div>
{resetError ? <ErrModal message={resetError} /> : null}
{resetSuccess ? <SuccessModal message={resetSuccess} /> : null}
<form onSubmit={handleForgot} className="reset-password">
{/* form content */}
</form>
</div>
)
}
从后端得到的错误或成功信息可以在用户界面中呈现,以让用户知道他们的行动的状态。
你会注意到我们正在使用自定义的模态组件来呈现信息。这些组件以道具的形式接收消息,并且它们可以在整个代码库中重复使用。看看下面这些组件的结构吧。
export const SuccessModal = ({ message }) => {
return (
<div className="auth-success-msg">
<p>{message}</p>
</div>
)
}
export const ErrModal = ({ message }) => {
return (
<div className="auth-err-msg">
<p>{message}</p>
</div>
)
}
你可以对这些组件进行独特的样式设计,这样你就可以将 "错误 "模态与 "成功 "模态区分开来。常见的惯例是用红色表示错误信息,用绿色表示成功信息。你如何选择这些组件的样式,完全取决于你。
除了上面所说的,我们还需要一种方法来验证正确的数据类型是否被作为道具传递给模态组件。这可以通过react中的 "prop-type "模块来实现。
propTypes.ErrModal = {
message: propTypes.string.isRequired,
}
propTypes.SuccessModal = {
message: propTypes.string.isRequired,
}
上面片段中的类型检查过程确保组件收到的数据必须是一个字符串,而且是必须的。如果该组件没有收到一个具有字符串值的道具,React将抛出一个错误。
现在我们已经涵盖了第一个表单的重要方面,以及我们将在reset-password路线中复制的构建块。让我们开始看看下面这个表单的布局。
import axios from "axios";
import React from “react”;
import Head from “next/head”;
import { useRouter } from "next/router";
import { SuccessModal, ErrModal } from "../components/Modals";
const ResetPassword = () => {
const [newPassword, setNewPassword] = React.useState("");
const [loading, setLoading] = React.useState(false);
const [resetPasswordSuccess, setResetPasswordSuccess] = React.useState();
const [resetPasswordError, setResetPasswordError] = React.useState();
const { query } = useRouter();
const token = query.token;
const email = query.email;
const resetPassword = () => { } // coming in later...
return (
<React.Fragment>
<Head>
<title>Reset your password</title>
</Head>
<div>
{email && token ? (
<div className="auth-wrapper">
{resetPasswordSuccess ? (
<SuccessModal message={resetPasswordSuccess} />
) : (
null
)}
{resetPasswordError ? (
<ErrModal message={resetPasswordError} />
) : (
null
)}
<form onSubmit={resetPassword} className="reset-password">
<h1>Reset Password</h1>
<p>Please enter your new password</p>
<div>
<label htmlFor="password">Password*</label>
<input
name="password"
type="password"
id="password"
placeholder="enter new pasword"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
/>
</input>
<button
name="reset-pwd-button"
className="reset-pwd"
>
{!loading ? "Reset" : "Processing..."}
</button>
</form>
</div>
) : (
<p>The page you're trying to get to isn't available</p>
)}
</div>
</React.Fragment>
);
};
由于我们在上一节中已经了解了第一个表单的基本情况,上面的片段几乎包含了前一个表单中的内容。
你可以看到我们如何从URL中读取参数,以及密码重置错误和成功变量的声明。
const [resetPasswordSuccess, setResetPasswordSuccess] = React.useState()
const [resetPasswordError, setResetPasswordError] = React.useState()
const { query } = useRouter()
const token = query.token
const email = query.email
你也会注意到我们是如何通过检查email 和token 变量是否存在于URL中来有条件地渲染重置密码表单的;如果这些变量是假的(即它们不在URL中),我们就渲染一个文本,说他们正在寻找的页面不可用。
{
email && token ? (
<div className="auth-wrapper">
<FormComponentt />
</div>
) : (
<p>The page you’re trying to get to isn’t available</p>
)
}
现在,让我们看一下处理函数,我们会用它来发送用户的新密码--加上用于验证的令牌和电子邮件--通过API端点发送到后端。
import { authEndpoints } from '../endpoints'
const resetPassword = async (e) => {
e.preventDefault()
try {
setLoading(true)
const response = await axios({
method: 'POST',
url: authEndpoints.resetPassword,
data: {
token,
email,
password: newPassword,
},
headers: {
'Content-Type': 'application/json',
},
})
setResetPasswordSuccess(response.data.msg)
setLoading(false)
setTimeout(() => {
router.push('/')
}, 4000)
setResetPasswordError('')
} catch (error) {
setLoading(false)
setResetPasswordError(error.response.data.msg)
setResetPasswordSuccess(null)
}
}
上面的片段是一个异步处理函数。我们用它来发送一个带有用户新密码、访问令牌和电子邮件地址的POST请求,这些都是我们从URL段的查询参数中抓取的。
setTimeout(() => {
router.push('/')
}, 4000)
当你看一下上面的片段,你会看到我们是如何使用JavaScript中的setTimeout 方法和Next.js的useRouter 钩子,在四秒后(如果你想的话,可以减少这个时间段)将用户重定向到主页--也就是本例中的登录页面,以便他们可以再次登录。
这样做也增加了良好的用户体验指标,因为它可以防止用户寻找一个链接或按钮,把他们带回登录页面。
最后的思考
外面有很多关于最佳实践和超强密码重置设计模式的信息。这篇文章只是一个密码重置流程的前端实现,其中也考虑了用户体验的问题。仅仅创建一个密码重置功能而不考虑使用这个功能的人的用户体验是不够的。
谢谢你的阅读。我希望这篇文章对您有所帮助!