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

文摘   2024-08-26 09:40   福建  

08. 使用 NextAuth 的 GoogleProvider 将用户添加到 Strapi 数据库

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

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

我们需要使用 jwt 回调函数将用户添加到 Strapi 的数据库中。我们也知道在什么时候执行这个操作。在上一章中,我们看到只有在触发 signin 事件时,jwt 回调中的 account 参数才会被填充。因此,我们将监听这些事件:

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

async jwt({ token, trigger, account, user, session }) {
  if (account) {
    if (account.provider === 'google') {
      // 我们现在知道正在使用 GoogleProvider 进行登录
      // 在这里处理 Strapi 相关逻辑
    }
  }
  return token;
}

Strapi 认证与授权

Strapi 通过 Users & Permissions 插件处理整个认证过程:

Strapi 提供了两种认证选项:一种是社交认证(Google、Twitter、Facebook 等),另一种是本地认证(电子邮件和密码)。这两种选项都会返回一个 JWT 令牌,该令牌将用于进行进一步的请求。(来源:https://strapi.io/blog/strapi-authentication-with-react)

本地认证的端点是 /api/auth/local(注意,这里是 Strapi 的后端),我们稍后将在设置凭据时使用它。社交认证的端点是 /api/connect/[providername],在我们的案例中,它将是 /api/connect/google

理论上,你不需要使用 NextAuth。你可以直接从前端调用 Strapi 的 Google 端点(/api/connect/google),Strapi 会启动整个认证过程,包括在首次登录时重定向到 Google 以请求权限。这是一个相当复杂的流程,Strapi 在文档中对此进行了很好的解释。

那么,为什么我们不使用这个流程并放弃 NextAuth 呢?(哦,好诱惑!)有几个非常好的理由:

  1. 你会直接将后端暴露给用户。
  2. 用户体验的混乱:用户会被重定向到不同的 URL,然后不得不经历一系列重定向。
  3. 缺乏错误处理。在此过程中出现的任何错误都会暴露给用户一个干巴巴的 JSON 错误。
  4. 你将失去 NextAuth 的所有优点:Cookie、会话访问、安全性、更新、自定义等。

集成 NextAuth 与 Strapi Users & Permissions 插件提供的认证

如前所述,你可以使用 Strapi 完成整个认证流程。这是一个多步骤的流程。而我们要做的是跳过 Strapi 认证流程的前几步。为什么?因为 NextAuth 处理了这些步骤。然后,我们在 NextAuth 内部连接到 Strapi 认证流程的后续步骤。

在我们的 NextAuth 中,目前代码如下:

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

async jwt({ token, trigger, account, profile, user, session }) {
  if (account) {
    if (account.provider === 'google') {
      // 我们现在知道正在使用 GoogleProvider 进行登录
      // 在这里处理 Strapi 相关逻辑
    }
  }
  return token;
}

Google 已经批准了我们的 OAuth 请求,并将一些数据返回给 NextAuth。这些数据对应于 jwt 回调中的 accountprofile 参数。在我们使用 Google 登录时,account 将被填充,而 account.provider 将是 google

现在,我们进入 Strapi 的认证流程。我们调用这个 Strapi 端点:/api/auth/google/callback?access_token=[>>google access token here<<],并附带 Google 访问令牌。那么这个访问令牌从哪里来?它来自 jwt 回调中的 account 参数。Google 将其发送回来了,而 NextAuth 让我们可以访问它。

当使用有效的令牌调用 Strapi 端点时,Strapi 会进行一些魔法操作,并将我们的 Google 用户作为 Strapi 用户添加到 Strapi 数据库中。然后,Strapi 会为该用户创建一个 JWT 令牌,并在 API 调用的响应中将其发送回我们。

async jwt({ token, trigger, account, profile, user, session }) {
  if (account) {
    if (account.provider === 'google') {
      // 成功后,我们将从这里接收 Strapi JWT 令牌
      const strapiResponse = await fetch(
        `${process.env.STRAPI_BACKEND_URL}/api/auth/google/callback?access_token=${account.access_token}`,
      );
    }
  }
  return token;
}

我们回到了 NextAuth(前端)并获得了一个新的 Strapi 令牌。我们需要对这个 Strapi 令牌做什么呢?我们需要将它放入我们的 NextAuth 令牌中。我们如何自定义 NextAuth 令牌?通过使用 jwt 回调函数。而我们是在 jwt 回调内部调用的这个 API 端点。这将完成我们的认证流程。

多种令牌

这里涉及到许多令牌操作,因此我再重复一下。我们的主要令牌是 NextAuth 令牌,它作为 Cookie 保存在浏览器中。这个令牌有一个有效载荷。默认情况下,NextAuth 会在其中放入一些内容,比如姓名和电子邮件。但我们还需要它包含 Strapi 令牌。

Strapi 令牌用于在后端(即 Strapi)中授权我们的前端用户。当向后端发出 API 请求时,我们需要在请求头中添加 Strapi 令牌。那么我们如何在前端访问这个令牌呢?我们将其保存到 NextAuth 令牌的有效载荷中。Strapi 令牌的有效载荷里是什么?不知道,也不关心。

最后是 Google OAuth 令牌。Google 在成功认证后会发送回这个令牌。这个认证过程由前端的 NextAuth 处理。要使用 Strapi 的社交提供商端点(/api/auth/google)创建一个 Strapi 用户,我们需要通过这个 API 端点将 Google OAuth 令牌发送到后端。Google 令牌的有效载荷里有什么?不知道,也不关心。Strapi 如何处理这个端点?不知道,也不关心。

设置 Strapi 的 Users & Permissions 插件

够多的理论了,让我们开始编码。我们从设置 Strapi 开始。我们需要在 Strapi 内部激活和配置 Google 提供商。因此,运行 Strapi(strapi develop),然后在浏览器中打开管理面板:http://localhost:1337/admin。

  1. 进入 Settings > Users & Permissions plugin > Providers > Google
  2. 切换启用开关
  3. 填写客户端 ID 和密钥,它们应该在你的前端环境文件中
  4. 忽略重定向 URL
  5. 保存

验证公共和认证角色的权限设置是否正确:

  • 进入 Settings > Users & Permissions plugin > Roles > public > permissions > User-permissions > auth
  • 进入 Settings > Users & Permissions plugin > Roles > authenticated > permissions > User-permissions > auth

所有认证选项都应该被允许:

最后,在前端环境文件中添加 STRAPI_BACKEND_URL=http://localhost:1337

添加 API 调用到 Strapi

接下来,我们编写 API 调用。由于它是一个 fetch 调用,我们将其封装在 try-catch 块中:

try {
  const strapiResponse = await fetch(
    `${process.env.STRAPI_BACKEND_URL}/api/auth/${account.provider}/callback?access_token=${account.access_token}`,
    { cache'no-cache' }
  );
  const data = await strapiResponse.json();
catch (error) {
  throw error;
}

注意我们在 fetch 中添加了 cache: 'no-cache' 选项。Next.js 使用了一种激进的缓存策略,因此我们避免了这种情况,以防万一。在 catch 块中,我们重新抛出错误。我们稍后会处理这些。

在实际的 API 调用中,URL 应该是合理的。那么 Strapi 会返回什么呢?成功时:返回一个 Strapi 用户,出错时:返回一个带有 error 属性的对象,而不是 Error。让我们为这些返回结果创建 Type 类型:

我们创建了一些类型文件:

// frontend/src/types/strapi/User.d.ts

export type StrapiUserT = {
  id: number;
  username: string;
  email: string;
  blocked: boolean;
  provider: 'local' | 'google';
};

export type StrapiLoginResponseT = {
  jwt: string;
  user: StrapiUserT;
};

我们还创建了一个 Strapi 错误类型。Strapi 的所有 API 路由的错误类型都是相同的,因此我们在这里创建一个通用的错误类型:

// frontend/src/types/strapi/StrapiError.d.ts

export type StrapiErrorT = {
  data: null;
  error: {
    status: number;
    name: string;
    message: string;
  };
};

我们可以用这些类型更新我们的 fetch 调用:

const

 strapiResponse = await fetch(
  `${process.env.STRAPI_BACKEND_URL}/api/auth/${account.provider}/callback?access_token=${account.access_token}`,
  { cache'no-cache' }
);
if (!strapiResponse.ok) {
  const strapiError: StrapiErrorT = await strapiResponse.json();
  throw new Error(strapiError.error.message);
}
const strapiLoginResponse: StrapiLoginResponseT = await strapiResponse.json();
// 自定义令牌

因此,如果 strapiResponse 不是 OK(状态码非 200),我们知道出现了问题,并抛出一个错误(稍后我们会处理这些错误)。否则,我们得到了 StrapiLoginResponseT 类型的数据,也就是一个用户和一个 JWT。我们现在将这些信息放入我们的 NextAuth 令牌中:token.strapiToken = strapiLoginResponse.jwt; 这样就完成了。记住,默认情况下,NextAuth 已经将姓名和电子邮件添加到了我们的令牌中。我们只是添加了 strapiToken。下面是整个 jwt 回调函数:

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

async jwt({ token, trigger, account, user, session }) {
  if (account) {
    if (account.provider === 'google') {
      // 我们现在知道正在使用 GoogleProvider 进行登录
      try {
        const strapiResponse = await fetch(
          `${process.env.STRAPI_BACKEND_URL}/api/auth/${account.provider}/callback?access_token=${account.access_token}`,
          { cache'no-cache' }
        );
        if (!strapiResponse.ok) {
          const strapiError: StrapiErrorT = await strapiResponse.json();
          throw new Error(strapiError.error.message);
        }
        const strapiLoginResponse: StrapiLoginResponseT =
          await strapiResponse.json();
        // 自定义令牌
        // 姓名和电子邮件已经在这里
        token.strapiToken = strapiLoginResponse.jwt;

      } catch (error) {
        throw error;
      }
    }
  }

  return token;
},

让我们运行这个代码吧!启动前后端的开发模式,然后执行登录流程。一切运行正常,但真正的证明在于 Strapi 管理面板:

进入 Content manager > collection types > user,你会看到我们的用户:

我们已经完成了大部分工作。我们完成了 jwt 回调函数,但还有一些内容缺失。我们还没有更新我们的会话。

编写 NextAuth 的会话回调

我们知道 jwt 回调在 session 回调之前运行,所以我们可以这样做:

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

async session({ token, session }) {
  session.strapiToken = token.strapiToken;
  return session;
},

这会触发一个 TypeScript 错误,但我们稍后会解决这个问题。让我们先使用我们的 <LoggedInClient /><LoggedInServer /> 组件来测试一下。我们取消注释 session 日志,并在注销并重新登录后检查我们的日志。正如预期的那样,session 现在显示一个额外的属性:strapiToken

{
  "user": {
    "name""Peter Jacxsens",
    "email""string",
    "image""string"
  },
  "strapiToken""string",
  "expires""Date"
}

更多数据

既然我们已经在做这个操作,让我们在令牌中添加更多的信息。我们将添加 provider(来自 account)、Strapi user iduser.blocked(来自 StrapiLoginResponse)。我们更新 jwt 回调:

token.provider = account.provider;
token.strapiUserId = strapiLoginResponse.user.id;
token.blocked = strapiLoginResponse.user.blocked;

并更新 session 回调:

session.provider = token.provider;
session.user.strapiUserId = token.strapiUserId;
session.user.blocked = token.blocked;

测试后,我们从 useSessiongetServerSession() 返回的 session 现在如预期一样:

{
  "user": {
    "name""Peter Jacxsens",
    "email""string",
    "image""string",
    "blocked""boolean",
    "strapiUserId""number"
  },
  "strapiToken""string",
  "provider""google",
  "expires""Date"
}

为 NextAuth 的会话和 JWT 回调参数设置类型

我们刚才做的自定义以及之前添加的 strapiToken 都会在 jwtsession 回调中触发 TypeScript 错误。回调参数列表中的属性在 NextAuth 内部某处接收类型定义。但 NextAuth 提供了扩展它们的机会。

注意:我不确定以下 TypeScript 定义是否完全正确。所有 TypeScript 错误都已解决,但请小心使用。

我们创建一个新的 TypeScript 定义文件:

// frontend/src/types/nextauth/next-auth.d.ts

// https://next-auth.js.org/getting-started/typescript
// 不确定这些是否正确,但它们不再引发 TS 错误

import NextAuth, { DefaultSession } from 'next-auth';
import { JWT } from 'next-auth/jwt';
import { StrapiUserT } from './strapi/StrapiLogin';

declare module 'next-auth' {
  // 由 `useSession`、`getSession` 返回,并作为 `SessionProvider` React Context 的参数接收

  interface Session {
    strapiToken?: string;
    provider?: 'google' | 'local';
    user: User;
  }

  /**
   * OAuth 提供商 `profile` 回调返回的用户对象的形状,
   * 或在使用数据库时 `session` 回调的第二个参数。
   */

  interface User extends DefaultSession['user'] {
    // 未设置这个会在授权函数中抛出 ts 错误
    strapiUserId?: number;
    blocked?: boolean;
  }
}

declare module 'next-auth/jwt' {
  // 由 `jwt` 回调和 `getToken` 返回的,当使用 JWT 会话时
  interface JWT {
    strapiUserId?: number;
    blocked?: boolean;
    strapiToken?: string;
    provider?: 'local' | 'google';
  }
}

你可以在 NextAuth 文档中阅读有关这些内容的全部内容,但我会简要解释一下。我们使用的语法称为模块增强(Module Augmentation)。这是 TypeScript 的一种功能,允许扩展(或合并)接口。这几乎就是我对它的全部了解。

至于内容,我们扩展了 3 个回调参数:tokenusersession。我们知道 session 返回类似这样的内容:{ strapiToken, provider, user: { ... }, ... }。看起来在内部,NextAuth 使用 jwt 回调 user 参数的类型来定义 session.user 的类型。因为我们在 user 上添加了额外的数据,我们必须使用 strapiUserIdblocked 扩展 DefaultUsername?:email?: 等),它们都是可选的。

然后我们更新 Session 接口,添加这个扩展后的 user 以及两个其他可选属性 strapiTokenprovider。最后,我们还扩展了 token 接口,添加了所有我们在其中放入的属性:strapiTokenstrapiUserIdproviderblocked,所有这些都是可选的。

这里有一个提示:在回调中输入 token.,然后 TypeScript 将建议所有可能的属性。如果你看不到你需要的属性或看到一个额外的属性,说明你做错了什么。

总结

我们刚刚学习了如何使用 GoogleProvider 将用户放入 Strapi 数据库。总体来说,这并不难。关键部分在于了解 NextAuth 如何处理不同的内容、在哪里处理、如何处理。

流程如下:

  1. NextAuth 向 Google OAuth 发出请求。
  2. Google OAuth 返回数据。
  3. NextAuth 在其回调函数中提供这些数据。
  4. jwt 回调中,我们检查 account 是否已定义(仅在登录时定义 account)。
  5. 我们使用 Google 令牌调用 Strapi 的 Google 提供商端点。
  6. Strapi 验证令牌并将用户添加到数据库。
  7. Strapi 将此用户数据和 Strapi JWT 令牌返回。
  8. 在 NextAuth 的 jwt 回调中,我们接收来自 Strapi 的数据和 Strapi JWT 令牌。
  9. 我们使用 jwt 回调将 Strapi JWT 令牌放入我们的 NextAuth JWT 令牌中。
  10. 我们使用 session 回调从前端读取此令牌,使用 useSession 钩子或 getServerSession 函数。
  11. 前端现在可以通过在请求头中添加 Strapi 令牌来向后端(Strapi)发出 API 请求。

最后需要

注意的是,这个 gist 帮助我理解了所有这些内容。

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

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