NextAuth v4 介绍
NextAuth 是一个开源的身份验证解决方案,适用于全栈(Next)应用程序。它支持不同的登录方式,如 OAuth 提供商(如 Google、GitHub 等)、凭据(经典的邮箱 + 密码)以及邮箱登录(通过用户点击的 "魔法链接")。它还提供不同的数据库适配器,可以直接将登录信息存入特定的数据库(如 MongoDB)或与 ORM(如 Prisma)集成。
在本系列中,我们将只关注使用 NextAuth 的一个 OAuth 提供商(Google)和凭据提供商(邮箱 + 密码)。
现在用简单的语言来解释
这是 NextAuth 的官方解释。但具体来说,它是做什么的呢?
1. 函数和处理器
使用 NextAuth 时,你的目标是构建身份验证系统:一个登录系统。一个登录页面,允许你点击按钮使用 Google 账户登录,或是一个表单,让你输入邮箱和密码然后提交。按钮和表单部分是纯粹的 Next 实现。NextAuth 为你提供了一些函数来调用,例如 signIn
和 signOut
。这些函数启动了身份验证流程。因此,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
部分包含了编码的数据,比如 userName
或 userId
等。
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 导入的 GoogleProvider
。clientId
和 secret
来自我们在第一章中编写的 .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 为我们提供了两种验证用户是否已登录的方法:
对于客户端组件: useSession
hook。对于服务器组件: getServerSession
函数。
NextAuth useSession
hook
useSession
是一个 hook,因此你只能在客户端组件中使用它。它只是一个 hook,所以你调用它(不需要参数),它会返回一个对象,你可以对其进行解构。
'use client';
import { useSession } from 'next-auth/react';
const { data: session, status, update } = useSession();
目前我们可以忽略 status
和 update
,重点关注 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
(未登录)或前面提到的默认会话。
注意 async
和 await
关键字!
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
设置为异步函数,但再次强调,不用过于担心这点,最终这只是一个配置。我们将在下一章继续。