本章的所有代码都可以在 GitHub 上找到,分支为 credentialssignin
。
https://github.com/peterlidee/NNAS/tree/credentialssignin
在当前状态下,应用程序实际上是可用的,只要你输入正确的电子邮件/用户名和密码即可。但我们尚未进行任何错误处理。那么,如果我们输入了错误的密码,会发生什么?让我们试一试。运行应用程序并输入错误的密码后,浏览器会重定向到 /authError
路由,并带有一个错误的搜索参数:Cannot read properties of undefined (reading 'username')
。
发生了什么?首先,/authError
页面是我们在前几章中设置的自定义 NextAuth 页面。NextAuth 仅在有限的情况下使用它,而这次显然就是其中之一。当然,这并不是向用户提供反馈的好方法。我们将自行处理这些错误,因此这个自定义的 NextAuth 错误页面将不再显示。尽管如此,很高兴看到 NextAuth 在背后支持我们。
错误信息 Cannot read properties of undefined (reading 'username')
来源于 authorize
函数,确切地说,是在我们返回用户数据的那一行:
return {
name: data.user.username,
//...
};
在 authorize
函数中,我们向 Strapi 发送了请求,并传递了错误的凭据。Strapi 返回了一个 strapiError
对象:
// frontend/src/types/strapi/StrapiError.d.ts
export type StrapiErrorT = {
data: null;
error: {
status: number;
name: string;
message: string;
};
};
数据是 null
。因此,当我们尝试访问 data.user.username
时,由于 data
为 null
,user
是 undefined
,这就是错误信息 Cannot read properties of undefined (reading 'username')
的来源。我们需要修复这个问题。
在 NextAuth 的 authorize 函数中处理错误
在之前的章节中,我们在 jwt
回调中处理了使用 GoogleProvider 的错误。那时,我们也需要处理一个可能返回 strapiError
的 Strapi 身份验证请求。在 jwt
回调中,我们通过抛出错误来处理这个错误。然后,我们可以通过读取 /signin
或 /authError
页面上的错误搜索参数在前端 UI 中处理此错误。我们已经见过这个过程,我不会重复。这里的重点是我们现在需要做类似的事情,再次在回调函数中处理 Strapi 身份验证请求的错误。
正如我们在 jwt
回调中所做的那样,我们可以通过抛出错误来中断身份验证流程。当在 authorize
中抛出错误时,身份验证流程将停止(不会再调用其他回调函数,用户也不会被登录),我们可以在 UI 中处理这个错误。
但是,还有另一种方法可以在 authorize
函数中停止身份验证流程:返回 null
。返回 null
也会停止身份验证流程,并通过我们在 jwt
回调中看到的错误搜索参数返回默认的 NextAuth 错误代码。
让我们演示一下。我们暂时修改 authorize
函数:
async authorize(credentials, req) {
return null;
}
当我们使用凭据登录时,authorize
函数将始终返回 null
。这将导致以下结果:
- 停止身份验证流程(用户不会被登录)。
- 通过错误搜索参数返回一个内部的 NextAuth 错误。
测试确认了这一点:
我们没有登录成功,也没有被重定向。我们的 URL 现在有了一个错误搜索参数:
http://localhost:3000/signin?error=CredentialsSignin
这个错误搜索参数的值是其中一个 NextAuth 错误代码:CredentialsSignin
。此外,我们最初为 GoogleProvider 设置的错误处理也适用于此,并将在表单下方显示错误信息。
快速回顾一下,我们可以通过返回 null
或抛出错误来中断 authorize
函数中的身份验证流程。返回 null
会触发 NextAuth 的内部错误处理,这与在 jwt
回调中抛出错误相似。
在 authorize
中抛出错误与在 jwt
回调中抛出错误的工作方式完全不同。让我们试试这个:
async authorize(credentials, req) {
throw new Error('foobar');
}
然后尝试登录:
我们被重定向到了 /authError
,并且有一个错误搜索参数:foobar
。请注意,这与本章开头的过程相同。
http://localhost:3000/authError?error=foobar
这里有个有趣的事情。我们可以手动处理这个错误。
使用 NextAuth 的 signIn 函数处理错误
我们可以通过在 signIn
函数中设置额外的参数 redirect: false
来防止 NextAuth 在 authorize
中抛出错误时进行重定向:
signIn('credentials', {
identifier: data.identifier,
password: data.password,
redirect: false,
});
这不仅改变了身份验证流程(通过不进行重定向),还改变了 signIn
的行为。signIn
现在将返回一个值:
{
error: string | undefined;
status: number;
ok: boolean;
url: string | null;
}
其中 error
将是我们在 authorize
中抛出的错误。这意味着我们现在可以在前端 UI 中直接访问 authorize
函数中抛出的错误。我们需要更新 signIn
以等待响应,并更新 handleSubmit
以使其成为异步函数:
const signInResponse = await signIn('signin', {
identifier: validatedFields.data.identifier,
password: validatedFields.data.password,
redirect: false,
});
console.log('signInResponse', signInResponse);
在当前的示例中,signInResponse
将如下所示:
{
"error": "foobar",
"status": 401,
"ok": false,
"url": null
}
总结
让我总结一下整个过程。我们需要在 authorize
函数中处理可能的 Strapi 错误,有三种方法可以停止 authorize
函数/回调的执行:
- 返回
null
会触发 NextAuth 的内部错误处理。我们将留在登录页面,并且 URL 中会添加一个错误搜索参数。 - 抛出错误会触发另一个内部的 NextAuth 错误处理过程。我们会被重定向到
authError
(一个自定义的 NextAuth 错误页面),并且再次获得一个错误搜索参数,这次的值等于我们在authorize
中抛出的错误的值。 - 抛出错误并在
signIn
函数中添加redirect: false
选项将跳过重定向和错误搜索参数。相反,signIn
现在将返回一个对象。然后我们可以使用这个返回值在表单组件中处理错误。
我花了很长时间来解释这个过程,但希望这对你有所帮助。这可能有点混乱,但我希望能把所有问题都讲清楚。
在继续之前,需要注意的是,能否将此过程用于 GoogleProvider?答案是否定的,redirect
选项仅适用于电子邮件和凭据提供者。
编码时间
现在是时候编写我们的代码了。首先我们在 authorize
函数中处理 Strapi 错误,然后我们再看表单组件。这是我们之前的代码:
async authorize(credentials, req) {
const strapiResponse = await fetch(
`${process.env.STRAPI_BACKEND_URL}/api/auth/local`,
{
method: 'POST',
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify({
identifier: credentials!.identifier,
password: credentials!.password,
}),
}
);
const data: StrapiLoginResponseT = await strapiResponse.json();
return {
name: data.user.username,
email: data.user.email,
id: data.user.id.toString(),
strapiUserId: data.user.id,
blocked: data.user.blocked,
strapiToken: data.jwt,
};
}
由于我们使用 fetch
,我们将其包装在 try-catch
块中。然后,我们将检查 strapiResponse
是否正常,如果不正常就进行处理(正常部分已经完成)。这是我们更新后的函数:
async authorize(credentials, req) {
try {
const strapiResponse = await fetch(
`${process.env.STRAPI_BACKEND_URL}/api/auth/local`,
{
method: 'POST',
headers: {
'Content-type': 'application/json',
},
body: JSON.stringify({
identifier: credentials!.identifier,
password: credentials!.password,
}),
}
);
if (!strapiResponse.ok) {
// 返回错误给 signIn 回调
const contentType = strapiResponse.headers.get('content-type');
if (contentType === 'application/json; charset=utf-8') {
const data: StrapiErrorT = await strapiResponse.json();
throw new Error(data.error.message);
} else {
throw new Error(strapiResponse.statusText);
}
}
// 成功
const data: StrapiLoginResponseT = await strapiResponse.json();
return {
name: data.user.username,
email: data.user.email,
id: data.user.id.toString(),
strapiUserId: data.user.id,
blocked: data.user.blocked,
strapiToken: data.jwt,
};
} catch (error) {
// 捕获 try 中的错误,或者 f.e. 连接失败
throw error;
}
}
这应该都很清楚。我们还会在 try-catch
块之前添加一行代码,测试是否存在凭据。我们将在调用 signIn
之前在表单组件中验证此内容,但这是额外的预防措施:
// 确保存在凭据
if (!credentials || !credentials.identifier || !credentials.password) {
return null;
}
为什么我们在这里返回 null
?因为由于我们之前的表单验证,这种情况很不可能发生,我们只需让 NextAuth 处理它即可。
在下一章中,我们将处理我们的表单组件。
首发于公众号 大迁世界,欢迎关注。📝 每周一篇实用的前端文章 🛠️ 分享值得关注的开发工具 ❓ 有疑问?我来回答
本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试完整考点、资料以及我的系列文章。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。