使用 Next 14 + NextAuth 4 + Strapi v4进行 Google 和凭据提供商身份验证的完整指南(02)

文摘   2024-08-20 10:02   福建  

NextAuth v4 介绍

NextAuth 是一个开源的身份验证解决方案,适用于全栈(Next)应用程序。它支持不同的登录方式,如 OAuth 提供商(如 Google、GitHub 等)、凭据(经典的邮箱 + 密码)以及邮箱登录(通过用户点击的 "魔法链接")。它还提供不同的数据库适配器,可以直接将登录信息存入特定的数据库(如 MongoDB)或与 ORM(如 Prisma)集成。

在本系列中,我们将只关注使用 NextAuth 的一个 OAuth 提供商(Google)和凭据提供商(邮箱 + 密码)。

现在用简单的语言来解释

这是 NextAuth 的官方解释。但具体来说,它是做什么的呢?

1. 函数和处理器

使用 NextAuth 时,你的目标是构建身份验证系统:一个登录系统。一个登录页面,允许你点击按钮使用 Google 账户登录,或是一个表单,让你输入邮箱和密码然后提交。按钮和表单部分是纯粹的 Next 实现。NextAuth 为你提供了一些函数来调用,例如 signInsignOut。这些函数启动了身份验证流程。因此,NextAuth 提供了与身份验证过程交互的函数。

2. 提供商

NextAuth 具有一系列内置的提供商。这些提供商允许你使用 Google、GitHub 或邮箱 + 密码等方式登录。NextAuth 已经为你准备好了这些功能,你不必去了解它们的内部工作原理。你只需要对提供商进行一些配置,然后它们就可以使用了。NextAuth 帮你处理了繁重的工作。

3. Cookies

在成功登录后,NextAuth 会在你的浏览器中设置一系列 cookies。这是 NextAuth 的第三个功能。你不需要担心这些 cookies,NextAuth 会为你处理。

4. 创建 Tokens

这些 cookies 之一包含了一个 JWT token。JWT 是一种编码(非加密)的字符串,看起来像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

它的结构是这样的:header.payload.signature,其中 payload 部分包含了编码的数据,比如 userNameuserId 等。

NextAuth 默认会将一些数据放入这个 token(payload)中,并允许你根据需要自定义这个 payload。这是 NextAuth 的第四个功能:创建 token。

5. 读取 Tokens

此外,NextAuth 还为你提供了工具,允许你检查是否存在 token 并从中检索数据。换句话说,它可以通过读取 token 来检查用户是否已登录。

6. 后端

这里缺少的是一个数据库。在本系列中,我们将与 Strapi 集成。因此,在登录过程中,我们需要将用户信息存入数据库,或检查该用户是否已注册。NextAuth 为你提供了实现这一点的功能。

NextAuth 功能概览

这就是 NextAuth 的主要功能(在本系列中):

  • 触发登录和登出过程的函数。
  • 可自定义的内置提供商和适配器。
  • 设置 cookies。
  • 创建和填充 JWT token。
  • 使用辅助函数读取这些 token。
  • 与数据库集成。

这大致上就是 NextAuth 的功能。它也应该能帮助你理解 Next 的边界在哪里,NextAuth 的起点在哪里。

路由处理器

NextAuth 的主要配置或自定义是在路由处理器中完成的。我们快速回顾一下什么是路由处理器。

在 Next 13+(app 路由器)中创建 API 路由,你需要使用路由处理器。它们类似于页面路由,但不是使用 page.tsx,而是在路由文件夹中使用 route.ts

例如,app/testing/route.ts 会创建一个 API 路由 http://localhost:3000/testing,你可以调用它。当然,你需要在 route.ts 文件中编写必要的代码,但你可以在 Next 的文档中了解所有相关内容。

NextAuth 路由处理器

在底层,NextAuth 会调用我们将要设置的 NextAuth 路由处理器。我们不必直接调用它,NextAuth 会为我们处理。但我们确实需要自己编写这个路由处理器,以完成大部分 NextAuth 的配置。

为什么这很重要?为了避免混淆。NextAuth 的主要配置和设置文件是一个路由处理器。为什么?因为这就是 NextAuth 的内部工作方式。

默认组件

我们提到过需要自己构建登录表单或按钮等组件,但这并不完全正确。NextAuth 有一些默认的登录组件,比如按钮和表单。但它们无法自定义,因此用途非常有限。

当我们开始构建项目时,我们将使用这些默认组件作为起点。这将使事情更简单。然后我们会逐步用自定义组件替换这些默认组件。

总结

花了一些额外的时间来写这一章,因为我在初次深入研究 NextAuth 时感到相当困惑。所以我试图澄清 NextAuth 的功能及其工作原理。

NextAuth 的功能可以总结如下:

  • 触发登录和登出过程的函数。
  • 可自定义的内置提供商和适配器。
  • 设置 cookies。
  • 创建和填充 JWT token。
  • 使用辅助函数读取这些 token。
  • 与数据库集成。

在底层,NextAuth 很大程度上依赖于一个自定义路由处理器,我们将在其中编写大量配置和自定义代码。

在下一章中,我们将开始编写代码。

使用 GoogleProvider 进行 NextAuth 登录

在本章中,我们将安装 NextAuth,并使用 Google 提供商设置一个基本示例。请注意,我们的 Next 应用程序目前只有一个页面,即首页(/app/index),以及一个包含指向首页链接的 <Navbar /> 组件。本章的最终代码可以在 GitHub 上找到(分支:basicgoogleprovider)。

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

安装 NextAuth

首先,在 frontend 文件夹中安装 NextAuth:

npm i next-auth

接下来,我们为 NextAuth 设置一个路由处理器。我们在 src/app/api/auth/[...nextauth]/route.ts 文件夹中创建一个 route.ts 文件。[...nextauth] 文件夹是 Next 中的一个 catch-all 路由,这意味着所有发往 api/auth 或例如 api/auth/local/register 的请求都会由我们新创建的 route.ts 路由处理器处理。将以下代码放入处理器中:

// frontend/src/app/api/auth/[...nextauth]/route.ts

import NextAuth from 'next-auth';
import { authOptions } from './authOptions';

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

接着,创建上面提到的 authOptions.ts 文件。

需要注意的是:authOptions 只是一个包含配置和自定义属性的对象。大多数教程直接在路由处理器中以对象字面量的形式编写它。然而,在运行 Next 构建时,它在某个点上给了我一个 TypeScript 错误。一位 NextAuth 的作者建议将 authOptions 放在一个单独的文件中。这确实解决了问题,这就是为什么我们将 authOptions 放在一个单独的文件中。

// frontend/src/app/api/auth/[...nextauth]/authOptions.ts

import { NextAuthOptions } from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';

export const authOptions: NextAuthOptions = {
  providers: [
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID ?? '',
      clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? '',
    }),
  ],
};

这里发生了什么:我们将第一个提供商添加到 providers 列表中。然后配置从 NextAuth 导入的 GoogleProviderclientIdsecret 来自我们在第一章中编写的 .env 文件。

顺便说一句,要查看 NextAuth 的文档,只需在 Google 上搜索 NextAuth provider google,它会引导你到文档页面,你可以在其中看到这个设置。

最后,将以下几行添加到 .env.local 文件中:(在开发环境中,这不是严格要求的,但会在终端中提示警告。)

# frontend/.env.local

NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=EDITME->somesecret

NEXTAUTH_SECRET 是你需要生成的一个密钥。对于 Windows 用户,打开 bash 并输入以下内容:

openssl rand -base64 32

将返回的字符串复制并粘贴到你的 .env.local 文件中。对于其他平台,我不太确定,可以查找相关信息。

使用 NextAuth 和 GoogleProvider 进行首次登录

我们现在已经准备好进行首次登录了。是的,就是这么简单。我们创建一个登录按钮:

// src/components/header/SignInButton.tsx

'use client';

import { signIn } from 'next-auth/react';

export default function SignInButton({
  return (
    <button
      type='button'
      className='bg-sky-400 rounded-md px-4 py-2'
      onClick={() =>
 signIn()}
    >
      sign in
    </button>

  );
}

首先,我们将其设置为一个客户端组件,因为我们的按钮需要一个事件处理器。在 onClick 事件中,我们传递了 signIn 函数。这是 NextAuth 提供的一个函数,当调用时会启动登录流程。

我们现在只有首页,但没关系。在当前的 NextAuth 配置中,没有自定义登录页面,NextAuth 会将我们重定向到一个默认的 NextAuth 登录页面。我们稍后会用我们自己的页面进行自定义。让我们运行 Next 前端并点击登录按钮。我们将被重定向到 http://localhost:3000/api/auth/signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2F(带有回调参数到首页):

这是 NextAuth 提供的默认登录页面,有几点需要注意:

  • 显然,这不是一个漂亮的设计。
  • 你不能通过其他样式来更改这个页面。
  • 如果你没有跟着编写代码,你不会知道这一点,但点击登录按钮会触发整个页面的重新加载(F5 重新加载)。

总之,这是你在生产环境中不想使用的东西,但在这里,它确实有些作用。我们点击 "Sign in with Google" 链接,看看会发生什么:

  • 我们被重定向到 https://accounts.google.com/o/oauth2/v2/auth/...(仅在首次登录时)。
  • 我们会被询问要使用哪个 Google 帐户(仅在首次登录时)。
  • 我们会被询问是否要登录到应用程序,并且 Google 会共享以下数据(仅在首次登录时)。这些与我们在第一章中进行的 Google OAuth 设置对齐。
  • 点击继续后,我们会被重定向回前端首页 localhost:3000

如果我们登出并再次登录(目前还不能),前 3 个步骤将被跳过。这只是经典的 Google 身份验证流程,我相信我们都经历过。

但是,它起作用了,所以很棒!不过我们缺少反馈。我们是否已登录?我们已经登录了,但我们不知道,这就是我们的下一步。

NextAuth Session

我们现在使用 NextAuth 登录了。从实际意义上讲,这意味着 NextAuth 通过 Google 验证了我们,然后设置了一些 cookies,其中一个包含 JWT token。但我们不必担心这些 cookies。

NextAuth 为我们提供了两种验证用户是否已登录的方法:

  1. 对于客户端组件:useSession hook。
  2. 对于服务器组件:getServerSession 函数。

NextAuth useSession hook

useSession 是一个 hook,因此你只能在客户端组件中使用它。它只是一个 hook,所以你调用它(不需要参数),它会返回一个对象,你可以对其进行解构。

'use client';
import { useSession } from 'next-auth/react';

const { data: session, status, update } = useSession();

目前我们可以忽略 statusupdate,重点关注 data 属性,我们将其重命名为 session。我们的 session 要么是 null/undefined(未登录),要么是一个类型为 DefaultSession 的对象:

{
  user?: {
    name?: string | null
    email?: string | null
    image?: string | null
  }
  expires?: string
}

这个 session 接口可以而且将会被自定义,意味着我们可以在其中放入更多数据。

NextAuth getServerSession() 函数

getServerSession 是一个异步函数,你可以在服务器组件或路由处理器中使用。

  • 它接受一个参数,即我们在 app/api/auth/[...nextAuth]/route.ts 路由处理器中传递的 authOptions 对象。
  • 它返回 null(未登录)或前面提到的默认会话。

注意 asyncawait 关键字!

import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/authOptions';

export default async function MyComponent({
  const session = await getServerSession(authOptions);
  // ...
}

我们将其添加到代码中,以更好地理解它。不过,首先要运行 useSession,我们必须将整个应用程序包裹在 NextAuth 提供的 SessionProvider 中。

NextAuth SessionProvider

这里事情有点复杂,但最终这只是一个配置,不必过于担心。

问题在于 NextAuth 提供的 SessionProvider 是一个客户端组件,因为它使用了 React hooks。但由于 NextAuth v4 已经有些年头了,它没有使用 use client 指令,这会导致 Next 抛出错误。

你可以在 Next 文档中阅读更多关于这个问题以及如何解决它的内容。解决方案是将 SessionProvider 导入到一个客户端组件中并立即导出它,如下所示:

// frontend/src/app/component/Provider.tsx

'use client';
import { SessionProvider } from 'next-auth/react';
export default SessionProvider;

我们现在可以使用这个 <Provider /> 组件来包裹我们的应用程序,在根布局文件中:

// frontend/src/app/layout.tsx

//...
import NavBar from '@/components/header/Navbar';
import { SessionProvider } from 'next-auth/react';
import { getServerSession } from 'next-auth';
import { authOptions } from './api/auth/[

...nextauth]/authOptions'
;
//...

export default async function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>
{
  const session = await getServerSession(authOptions);
  return (
    <html lang='en'>
      <body className={`${inter.className}  px-2 bg-zinc-200`}>
        <SessionProvider session={session}>
          <div className='max-w-6xl mx-auto'>
            <NavBar />
            <main className='my-4'>{children}</main>
          </div>
        </SessionProvider>
      </body>
    </html>

  );
}

注意我们如何将 RootLayout 设置为异步函数,但再次强调,不用过于担心这点,最终这只是一个配置。我们将在下一章继续。

最后:
CSS技巧与案例详解
vue2与vue3技巧合集
VueUse源码解读


JavaScript 每日一练
每天一道JavaScript 实战题,让大家平时多多积累实用的知识,提高开发效率,才有更多的时间摸鱼。
 最新文章