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

06.NextAuth:创建自定义登录页面
到目前为止,我们一直在使用 NextAuth 提供的默认登录页面。但我们遇到了两个问题:
这个页面不美观,无法自定义。
页面会重新加载,影响用户体验。
为了解决这些问题,我们将实现一个自定义的登录页面。为此,我们需要完成以下几步:
创建一个登录页面(一个表单组件)。
创建一个 NextAuth 处理器。
配置一些 NextAuth 设置。
本章的代码可以在 GitHub 上找到:分支 customlogin。
创建登录页面
我们首先创建一个新的页面:
// frontend/src/app/(auth)/signin/page.tsx

import SignIn from ‘@/components/auth/signIn/SignIn’;

export default function SignInPage() {
return ;
}
请注意,我们在这里使用了一个路由组 (auth)。我们将来可能会有更多的身份验证页面,例如注册页面,因此我希望将它们分组,而不在实际路由中体现。这意味着我们的登录页面将位于 http://localhost:3000/signin。
我们在这个页面中除了导入 组件外,不做其他操作。我喜欢将 app 文件夹专用于路由,并将其他内容移动到组件中。
组件
在 components 文件夹中,我们创建一个 auth 文件夹来分组所有身份验证相关的组件。以下是我们的 组件:
// frontend/src/components/auth/signIn/SignIn.tsx

import Link from ‘next/link’;

export default function SignIn() {
return (

Sign in

Sign in to your account or{‘ ‘} create a new account.

[form]

or G Sign in with Google

);
}
它看起来像这样:

几个注意点:我知道它的样式不太好看,但重点是你可以自行定制它。由于我们稍后会使用凭据登录,我已经为表单和尚不存在的注册页面添加了一些占位符。
使用 Google 登录按钮
我们已经在 组件中使用了 NextAuth 提供的 signIn 函数。调用 signIn() 只会将我们重定向到默认的 NextAuth 登录页面。
但是,signIn 函数有第二种用法。当我们调用它并传递一个提供商名称时,它会为该提供商启动身份验证流程:
signIn(‘google’);
它可以接受一个可选的第二个参数,即选项对象。我们将在稍后详细介绍。
我们在按钮点击时调用 signIn,因此需要一个客户端组件。我们将 Google 登录按钮移到一个单独的客户端组件中,附上我们的 signIn 函数,并将其导入到 组件中:
// frontend/src/components/signIn/GoogleSignInButton.tsx

‘use client’;

import { signIn } from ‘next-auth/react’;

export default function GoogleSignInButton() {
return (
signIn(‘google’)} > G Sign in with Google
);
}
注册我们的自定义登录页面
我们还不能进行测试。如果我们现在运行应用程序(未登录)并点击导航栏中的登录按钮,它仍会将我们带到默认的 NextAuth 登录页面。我们需要告诉 NextAuth 我们创建了一个自定义页面。我们在 authOptions 对象中进行设置。
我们向 authOptions 对象添加一个新的 pages 属性,其中我们将 signin 属性设置为我们的登录页面:
// frontend/src/app/api/auth/[…nextauth]/authOptions.ts

export const authOptions: NextAuthOptions = {
//…
pages: {
signIn: ‘/signin’,
},
};
太好了!运行应用程序,登出。点击登录,我们将被重定向到我们自定义的登录页面。但是,页面发生了完整的重新加载。此外,我们的 URL 现在是:http://localhost:3000/signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2F。它将一个 callbackUrl 参数添加回我们的首页。
点击 “Sign in with Google” 按钮,会发生以下情况:
页面重新加载。
我们没有被重定向。
但我们确实登录了!

所以,它起作用了,但我们还有一些问题需要解决。
NextAuth 重定向
signIn 函数的第二个参数是一个选项对象。其中一个选项是 callbackUrl:
signIn(‘google’, { callbackUrl: ‘someUrl’ });
我们之前遇到过这个问题。NextAuth 自动将 callbackUrl 作为参数添加到 URL 中。老实说,我期望在我们当前的设置下(没有选项对象)被重定向到主页。NextAuth 文档中提到:
callbackUrl 指定用户登录后将被重定向到的 URL。默认为启动登录的页面 URL。
基于此,我本来以为会默认重定向到主页。但事实并非如此。因此,让我们更新 signIn 函数,将 callbackUrl 设置为主页并进行测试:
// frontend/src/components/signIn/GoogleSignInButton.tsx

onClick={() => signIn(‘google’, { callbackUrl: ‘/’ })}
它起作用了,我们得到了重定向。需要注意的是,NextAuth 只接受相对路径或与应用程序相同域上的绝对 URL。始终重定向到主页是可能的,但在我们的情况下,我们希望将用户重定向回他或她之前所在的页面。我们使用 useSearchParams hook 更新函数:
// frontend/src/components/signIn/GoogleSignInButton.tsx

‘use client’;

import { signIn } from ‘next-auth/react’;
import { useSearchParams } from ‘next/navigation’;

export default function GoogleSignInButton() {
const searchParams = useSearchParams();
const callbackUrl = searchParams.get(‘callbackUrl’) || ‘/’;
return (
signIn(‘google’, { callbackUrl })} > G Sign in with Google
);
}
我们从 searchParams 获取 callbackUrl 并简单地将其传递给 signIn 选项。我们还为我们的主页路由提供了一个备用 ‘/’。searchParams 中的 callbackUrl 可能为空,例如当用户直接访问 localhost:3000/signin 时。
要测试这一点,我们创建了一个小测试页面,这样我们就可以从那里登录:
// frontend/src/app/test/page.tsx

export default function page() {
return

test page;
}
我们打开 localhost:3000/test,登出,登录,它将我们再次重定向到 /test。非常好!我尝试添加一些查询参数(/test?foo=bar),这些参数也没有问题地传递下来。最后,我直接打开了 /signin,登出并重新登录,正如预期的那样,我们被发送到了首页。
注意:还有另一个 signIn 选项,称为 redirect,它接受一个布尔值。但这个选项只对邮箱和凭据提供商有效。我们稍后会使用这个选项。
保护登录页面
最后一个细节。我们将更新我们的 组件。如果用户已经登录,我们将显示一条消息,告知用户“您已登录”,而不是显示 Google 登录按钮。为什么?为了不让用户感到困惑。

// frontend/src/components/signIn/SignIn.tsx

import Link from ‘next/link’;
import GoogleSignInButton from ‘./GoogleSignInButton’;
import { getServerSession } from ‘next-auth’;
import { authOptions } from ‘@/app/api/auth/[…nextauth]/authOptions’;

export default async function SignIn() {
const session = await getServerSession(authOptions);
return (

Sign in

{session ? (

You are already signed in. ) : (

)}
);
}

处理页面重新加载
即使使用我们的自定义登录页面,当点击以下按钮时,页面仍会完全重新加载:
登录按钮
使用 Google 登录按钮
登出按钮
这有什么问题吗?有点问题。整个应用程序会重新加载,你必须重新下载所有内容,屏幕在组件挂载时会闪烁,加载时间也会增加。这不是一个理想的流程。但除了这些问题,也不是特别严重。我们可以修复它们吗?我花了很多时间研究和尝试各种方法。
点击 Google 登录按钮时的重新加载是无法修复的。但这是正常的情况,每个 Web 应用在使用 Google 登录时都会重新加载。登出按钮同样无法修复,会有一次重新加载,但这也是正常行为。最后,我们可以修复登录按钮的重新加载问题。
修复点击登录按钮时的重新加载问题
你可能已经猜到了。为什么不直接链接到我们的登录页面,而不是使用调用 signIn 的按钮呢?我们可以这样做,但会引发一些问题。
第一个问题是,我们失去了 callbackUrl 参数。前面提到,当调用 signIn 时,NextAuth 会自动添加一个 callbackUrl 查询参数。当我们移除按钮并使用 Link 时,显然不再有这个 callbackUrl。我们必须自己构建它。因此,我们将用一个新组件 替换 :
// frontend/src/components/header/SignInLink.tsx

‘use client’;

import Link from ‘next/link’;
import useCallbackUrl from ‘@/hooks/useCallbackUrl’;

export default function SignInLink() {
const callbackUrl = useCallbackUrl();
return (
/signin?callbackUrl=${callbackUrl}} className=’bg-sky-400 rounded-md px-4 py-2′ > sign in
);
}
记住,这是我们的导航栏组件中的登录链接。因此,我们需要链接到 /signin。但我们还需要添加一个 callbackUrl 查询参数。这个参数的值是什么?我们当前的 URL 加上它自己的所有查询参数。以下是一个示例:如果我们在 /test?foo=bar 页面,我们希望 链接到 /signIn 加上 ?callbackUrl=/test?foo=bar。为了创建 callbackUrl 值,我们编写了一个自定义 hook,以便能够重复使用。以下是该 hook:
// frontend/src/hooks/useCallbackUrl.ts

import { usePathname, useSearchParams } from ‘next/navigation’;

export default function useCallbackUrl() {
const pathname = usePathname();
const params = useSearchParams().toString();
// 如果没有查询参数,不添加 ?
const callbackUrl = ${pathname}${params ? '?' : ''}${params};
return callbackUrl;
}
一些注意事项:
我们使用了相对 URL。
不需要 encodeURIComponent,因为 Next 在 usePathname 和 useSearchParams hooks 中已经处理了这点。
这里有一个缺陷。如果我们从 /test 页面通过新链接进入登录页面,我们将得到以下路由:/signin?callbackUrl=/test。如果我们再次点击导航栏中的登录按钮,我们的路由将变成:/signin?callbackUrl=/signin?callbackUrl=%2Ftest。callbackUrl 变成了当前页面的 URL 加上 callbackUrl 参数。这就像一个反馈回路。有趣的是,NextAuth 的 signIn 函数也有同样的问题。所以,我决定忽略这个问题。
好的一面是:我们可以运行这个功能并且它有效!进入登录页面不再重新加载页面。进一步测试,callbackUrl 也能正常工作。登录后,我们正确地重定向到了对应的页面(但会有一次完整的页面重新加载,这是预期的行为)。
总结
我们制作了一个自定义登录页面。这是绝对必要的,且实现起来并不困难。我们还学习了如何使用 signIn 函数与提供商及选项参数。通过此功能,我们可以手动启动 NextAuth 的身份验证流程。选项对象包括 callbackUrl 查询参数。
最后,我们解决了部分登录和登出流程中的页面重新加载问题。我们只解决了导航到登录页面的问题。这引发了一个新的复杂性,即我们需要手动将 callbackUrl 参数传递给 URL。
这样做值得吗?自定义登录页面是必须的!而且硬性重新加载在我看来很令人不悦。我很高兴我们至少解决了其中一个问题。此外,我们用最少的代码解决了这个问题。本章内容相对较多,但最终代码是可管理的,解决方案也很不错。
在下一章中,我们将开始与 Strapi 的集成。

声明:文中观点不代表本站立场。本文传送门:https://eyangzhen.com/420463.html

联系我们
联系我们
分享本页
返回顶部