本章的所有代码都可以在 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 时,由于 datanulluserundefined,这就是错误信息 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 错误。

测试确认了这一点:

image.png

我们没有登录成功,也没有被重定向。我们的 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');
}

然后尝试登录:

image.png

我们被重定向到了 /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 函数/回调的执行:

  1. 返回 null 会触发 NextAuth 的内部错误处理。我们将留在登录页面,并且 URL 中会添加一个错误搜索参数。
  2. 抛出错误会触发另一个内部的 NextAuth 错误处理过程。我们会被重定向到 authError(一个自定义的 NextAuth 错误页面),并且再次获得一个错误搜索参数,这次的值等于我们在 authorize 中抛出的错误的值。
  3. 抛出错误并在 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 已收录,有一线大厂面试完整考点、资料以及我的系列文章。


王大冶
68.1k 声望105k 粉丝