这一章比较棘手,因为我们需要引入一些新的概念。此外,NextAuth 提供的错误反馈非常有限,这让人有些困惑。我们将从一些稍微偏题的点开始,然后回到错误处理上来。

最终的代码可以在 GitHub 上找到(分支:callbacksForGoogleProvider)。

https://github.com/peterlidee/NNAS/tree/callbacksForGoogleProvider

Next.js 错误边界

我们从在 Next.js 前端添加一些错误处理开始,通过在项目的根目录中添加 error.tsx 文件。我们使用文档中的基本示例:

// frontend/src/app/error.tsx

'use client'; // 错误组件必须是客户端组件

import { useEffect } from 'react';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string },
  reset: () => void,
}) {
  useEffect(() => {
    // 将错误记录到错误报告服务
    console.error(error);
  }, [error]);

  return (
    <div>
      <h2>出了些问题!</h2>
      <p>根错误:{error.message}</p>
      <button
        onClick={
          // 尝试通过重新渲染该部分来恢复
          () => reset()
        }
      >
        再试一次
      </button>
    </div>
  );
}

当出现未捕获的错误时,这个组件将会捕获它。这与主题有点偏离,因为 NextAuth 的 GoogleProvider 不会产生未捕获的错误。但由于这一章是关于错误的,我们在这里添加了这个组件。

NextAuth 的 signIn 回调

我们在之前的章节中简要提到过这个回调:该回调允许你控制用户是否被允许登录。

async signIn({ user, account, profile, email, credentials }) {
  return true;
}

我们为什么需要这个?假设用户创建了一个 Google 账户,但没有验证该账户。我们不希望用户使用这个未验证的账户连接到我们的应用程序。signIn 回调有许多参数,我们之前已经介绍过。profile 是其中之一,它包含 Google 返回的原始用户数据。profile 有一个属性 email_verified。我们可以这样做:

async signIn({ user, account, profile, email, credentials }) {
  if (!profile.email_verified) return false;
  return true;
}

返回 false 会阻止该用户的整个认证流程。此外,用户将被重定向到默认或自定义的 NextAuth 错误页面。

在 NextAuth 中创建自定义错误页面

除了有默认的登录页面(我们之前替换过的丑陋的那个),NextAuth 还有一个默认的错误页面。我们可以并且将会替换这个默认页面为自定义错误页面。注意,这与我们之前创建的 error.tsx 不同。

NextAuth 在何时使用此页面?文档指出,当发生以下情况时,用户将被重定向到此错误页面:

  • authOptions 中的配置错误。
  • AccessDenied 错误:当你通过 signIn 回调或 redirect 回调限制了访问时。
  • 验证错误:与电子邮件提供程序相关。
  • 默认错误:其他一些情况。

我们刚刚看到了 signIn 回调。当它返回 false 时,我们会被重定向到错误页面。所以,让我们试一下,在 signIn 回调中返回 false

// frontend/src/api/auth/[...nextAuth]/authOptions.ts
{
  async signIn({ user, account, profile, email, credentials }) {
    return false;
  }
}

我们只是想看到错误页面,所以我们暂时对所有内容返回 false 并尝试登录。果然如预期,我们被重定向到默认的错误页面:

image.png

NextAuth 默认错误页面

它和我们之前看到的默认登录页面风格一样“经典”。但我们是不是缺少了错误信息?并没有,NextAuth 将其放在了 URL 中的 error 查询参数中:?error=AccessDenied

http://localhost:3000/api/auth/error?error=AccessDenied

那么剩下的消息在哪里?这就是全部了。我们稍后会回到这一点。首先我们完成这个错误页面部分。

自定义错误页面

和登录页面一样,我们也可以创建一个自定义错误页面。创建一个页面:

// frontend/src/app/(auth)/authError/page.tsx

type Props = {
  searchParams: {
    error?: string,
  },
};

export default function AuthErrorPage({ searchParams }: Props) {
  return (
    <div className='bg-zinc-100 rounded-sm p-4 mb-4'>
      AuthError: {searchParams.error}
    </div>
  );
}

然后在 authOptions.page 中告诉 NextAuth 使用此页面:

// frontend/src/api/auth/[...nextAuth]/authOptions.ts
pages: {
  signIn: '/signin',
  error: '/authError',
},

image.png

我们测试一下,所有都正常工作。NextAuth 现在使用我们的自定义错误页面。如前所述,我们稍后会处理错误消息。

总结

我们发现 NextAuth 有一个错误页面。当发生以下情况时,我们会被重定向到此页面:

  • authOptions 中存在配置错误 (/authError?error=Configuration)。
  • 我们使用了 signInredirect 回调 (/authError?error=AccessDenied)。
  • 存在验证错误 (/authError?error=Verification)。
  • 还有其他错误 (/authError?error=Default)。

我们已经用一个自定义页面替换了默认的错误页面。这样我们就处理了在 NextAuth 中可能遇到的一些错误。在继续之前,我们还需要提到一些其他要点。

signIn 回调中,当我们不希望用户能够通过认证时,我们返回 false。但还有另一种选择,我们也可以返回一个相对路径。这将覆盖错误页面,并将用户重定向到指定的路径。例如,你可以将用户重定向到专门为此目的创建的另一个路由。

最后,我们实际实现前面提到的示例,即未验证的 Google 账户。我们更新 signIn 回调:

async signIn({ user, account, profile }) {
  // console.log('singIn callback', { account, profile, user });
  if (
    account &&
    account.provider === 'google' &&
    profile &&
    'email_verified' in profile
  ) {
    if (!profile.email_verified) return false;
  }
  return true;
}

(我们不得不添加一些类型检查,因为 TypeScript 对我们发出了一些警告。)

其他错误

我们发出的每个请求都有可能失败或返回错误。我们应该考虑到这一点。在我们到目前为止的登录流程中,我们使用了 4 个请求:

  1. 发起登录请求。
  2. 发起登出请求。
  3. 调用 Google。
  4. 调用 Strapi。
调用 signInsignOut

NextAuth 内部使用了一个 REST API 端点来处理其所有的流程。这意味着对该端点的请求可能会出错。

我试着制造一个错误,例如 signIn('foobar', {...}),但什么都没有发生。没有控制台或终端中的错误,也没有 URL 中的错误参数。这让我得出结论,你可以安全地调用 signInsignOut 而不必试图捕捉错误。

Google OAuth 请求

当使用 GoogleProvider 登录时,NextAuth 会在某个时间点向 Google OAuth 发起请求。由于这是一个请求,所以它可能出错。这些 OAuth 错误可能包括:

  • 未验证的应用程序。
  • 无效的令牌。
  • 不正确的回调。

这些都是可能的错误。那么我们该如何处理这些错误呢?首先,让我们制造一个错误。在 authOptions 中,我们将 clientSecret 替换为一个随机字符串,看看会发生什么:

// clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? '',
clientSecret: 'foobar',

我们运行应用程序并进行登录。结果表明,表面上似乎没有发生什么变化。我们没有登录,没有错误弹出,应用程序没有崩溃,也没有被重定向。但有一些事情发生了。首先,我们的 URL 现在看起来像这样:

http://localhost:3000/signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2F&error=OAuthCallback
// 解码后
http://localhost:3000/signin?callbackUrl=http://localhost:3000/&error=OAuthCallback

所以,基础 URL 是 http://localhost:3000/signin,我们有两个查询参数:callbackUrlerrorerror 的值是 OAuthCallback。其次,在我们的前端终端中,出现了完整的错误信息:

[next-auth][error][OAUTH_CALLBACK_ERROR]
https://next-auth.js.org/errors#oauth_callback_error invalid_client (Unauthorized) {
  error

: OPError: invalid_client (Unauthorized)
      at processResponse ...
      ...
  providerId: 'google',
  message: 'invalid_client (Unauthorized)'
}

所以,NextAuth 记录了错误为 OAUTH_CALLBACK_ERROR,而原始的 Google OAuth 错误可能是 invalid_client (Unauthorized)。这发生在终端中。在我们的客户端(浏览器)中,NextAuth 给了我们一个错误参数:error=OAuthCallback。(浏览器控制台中没有错误日志!)

NextAuth 错误和错误代码

NextAuth 区分了错误和错误代码。OAUTH_CALLBACK_ERROR 是一个 NextAuth 错误,并在终端中记录下来。?error=OAuthCallback 是一个 NextAuth 错误代码,它被作为查询参数添加到回调 URL 中。

认证流程中的任何问题都会被 NextAuth 捕获,并分类为一个 NextAuth 错误。你可以在文档中找到这些错误的完整列表。

https://next-auth.js.org/errors

关于错误代码,NextAuth 这样说:

为了提高安全性,我们故意限制了返回的错误代码。

NextAuth 将错误作为查询参数传递到两个页面:

  1. 默认或自定义登录页面。
  2. 默认或自定义错误页面。

上述示例是一个作为查询参数发送到我们的自定义登录页面的错误。我们创建了一个自定义登录页面(/signin),并接收到了一个错误:/signin?error=OAuthCallback。之前,我们已经讨论过传递给 NextAuth 错误页面的错误(例如 ConfigurationAccessDenied)。

最小的错误消息

因此,NextAuth 错误(终端)非常详细,但我们无法将其用于用户反馈(这是有意为之的)。那么它们的作用是什么?我不确定。也许是用于开发过程中解决问题。在生产环境中,可以通过检查日志来解决问题。

然而,我们仍然需要一些用户反馈。NextAuth 仅提供了一个单词的错误代码,例如 OAuthCallbackcallbackAccessDenied。因此,作为开发者,你需要为每个代码想出一些巧妙的错误消息。例如:

const errorMap = {
  'OAuthCallback': '这里是一个非常巧妙且用户体验友好的消息。',
  'callback': '这是另一个示例消息。',
  'AccessDenied': '还有更多吗?',
};

// 在前端调用它们
{errorMap[searchParams.error]}

所以,祝你好运。

处理 NextAuth 中的登录错误

我们仍然需要实际在登录页面上显示错误。创建一个新的客户端组件:

// frontend/src/components/auth/signin/GoogleSignInError.tsx

'use client';

import { useSearchParams } from 'next/navigation';

export default function GoogleSignInError() {
  const searchParams = useSearchParams();
  const error = searchParams.get('error');
  if (!error) return null;
  return (
    <div className='text-center p-2 text-red-600 my-2'>
      出了点问题!{error}
    </div>
  );
}

注意我们不打算编写更好的错误消息,这超出了本系列的范围。我们将此组件添加到 <SignIn /> 组件中,完成:

//...
  <GoogleSignInButton />
  <GoogleSignInError />
// ...

image.png

在 NextAuth 中处理 Strapi 错误

有一件事我们还没有检查。在本文前面部分,我们提到我们在应用程序中发出了 4 个请求:登录和登出、Google OAuth 和 Strapi。我们还没有测试 Strapi 错误是否得到了处理。

authOptions 中,GoogleProvider,移除之前我们设置为 foobar 的错误 googleClient,以引发错误。然后,为了从 Strapi 引发错误,我们将一个随机字符串作为 access_token(我们通常从 Google OAuth 获得该令牌)。

// frontend/src/api/auth/[...nextAuth]/authOptions.ts

//`${process.env.STRAPI_BACKEND_URL}/api/auth/${account.provider}/callback?access_token=${account.access_token}`,
`${process.env.STRAPI_BACKEND_URL}/api/auth/${account.provider}/callback?access_token=foobar`,

我们期待什么?在 URL 中出现一些类型的 NextAuth 错误代码。我们运行应用程序并尝试登录。果然如预期,URL 中出现了一个错误:error=Callback。在文档中查找这个错误:

Callback: Error in the OAuth callback handler route

此外,我们还在终端中得到了真正的错误:

[next-auth][error][OAUTH_CALLBACK_HANDLER_ERROR]
https://next-auth.js.org/errors#oauth_callback_handler_error 400 Bad Request {
  message: '400 Bad Request',
  ...
}

[OAUTH_CALLBACK_HANDLER_ERROR] 是 NextAuth 处理此错误的方式。400 Bad Request 来自 Strapi:我们在此处抛出了它 throw new Error(strapiError.error.message);

因此,我们已经处理了潜在的 Strapi 错误。

结论

我们学习了 NextAuth 如何处理错误。在大多数情况下,它只是将错误代码添加到登录页面的 URL 中:?error=。在某些情况下,它会重定向到默认或自定义的 NextAuth 错误页面,同样带有错误代码。你可以在文档中查找这些代码,并使用错误代码为用户提供一些错误反馈。

此外,NextAuth 还会在你的 Next 服务器的终端中记录更详细的错误信息。这些错误也有特定的错误名称,你可以在文档中查找。然而,你不能将这些错误用于用户反馈。

如果我们从更实际的角度来看,显然 NextAuth 处理了所有错误。我们不必捕捉任何东西。这很好。NextAuth 有意在前端限制了错误信息。这可能会有点让人沮丧,并且需要一些努力来处理。但是,最终,NextAuth 非常稳定。使用 GoogleProvider 时获取错误应该是罕见的,并且这些错误现在已经得到了处理。

这就结束了我们与 GoogleProvider 的工作。在本系列的其余部分,我们将处理凭据提供程序。

首发于公众号 大迁世界,欢迎关注。📝 每周一篇实用的前端文章 🛠️ 分享值得关注的开发工具 ❓ 有疑问?我来回答

本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试完整考点、资料以及我的系列文章。


王大冶
68k 声望104.9k 粉丝