为了账号安全,请及时绑定邮箱和手机立即绑定

Convex与Kinde集成详解

标签:
云计算 API

本指南概述了针对 Kinde 的特定设置,流程类似Convex & Clerk集成,但本指南的重点是讲解如何将 Kinde 集成到 Convex 中。

它解答了许多开发者的问题,这些问题是由Kinde开发者社区关心的。详情可在这里查看:Kinde 社区 - 将 Convex 与 Kinde 集成

该教程清晰地列出了如何使用Kinde认证与Convex集成的实际步骤,并按照最佳实践。

Kinde 是一个认证平台,支持无密码登录方式,如魔法链接(Magic Link)、短信验证码(SMS)或身份验证器应用程序(Authenticator App)。它还支持多因素认证(MFA)以增强安全性,支持基于 SAML 的企业级单点登录(SSO),并提供强大的用户管理工具,帮助企业更好地管理用户。

示例: 使用Kinde的凸形认证(点击这里:Convex Authentication with Kinde

如果你在使用 Next.js,可以参考 Convex 的 Next.js 设置指南

开始吧

此指南假设你已经集成了 Convex 的可正常运行的 Next.js 应用。如果没有,请先参考 Convex Next.js 快速入门。然后:

  • 注册 Kinde 账号

kinde.com/register 注册一个免费的 Kinde 账户。

Kindle注册页面

  • 在Kindle创办一个企业

输入您公司或应用的名称:

在Kinde上创建业务

  • 选择你的技术组合

选择你用来构建此应用程序的技术栈和工具。

在Kinde上选择技术栈(Tech Stack)

  • 选择认证方式

选择你想要用户如何签入。

选择验证方式

  • 将您的应用连接到Kinde

让你的 Next.js 应用连接到 Kinde。

展示如何将一个 Next.js 应用连接至 Kinde

  • 创建认证设置

从你的 .env.local 文件中复制 KINDE_ISSUER_URL 这个字段。进入 convex 文件夹后,然后创建一个新的 auth.config.ts 文件,用于设置服务器端验证访问令牌的配置信息。

复制你的Kindle发行人的链接

粘贴 _KINDE_ISSUERURL 并将 applicationID 设置为 "convex" (无需修改 "aud" Claims 字段的值)。

    const authConfig = {
      providers: [
        {
          domain: process.env.KINDE_ISSUER_URL, // 例如:https://barswype.kinde.com
          applicationID: "应用ID",
        },
      ]
    };

    export default authConfig;

点击这里进入全屏模式 点击这里退出全屏模式

  • 设置 Convex & Kinde Webhook

在 Kinde 仪表盘中,进入 设置 > Webhooks > 点击 添加 Webhook > 命名为 Webhook 并粘贴您的 Convex 端点 URL,例如 https://<您的-convex-app>.convex.site/kinde

选择要触发的事件,比如“user.created”和“user.deleted”。

现在回到你的代码部分。打开你的 convex/ 文件夹,然后新建一个名为 http.ts 的文件,接着复制并粘贴这段代码。

    import { httpRouter } from "convex/server";
    import { internal } from "./_generated/api";
    import { httpAction } from "./_generated/server";
    import { jwtVerify, createRemoteJWKSet } from "jose";

    type KindeEventData = {
      user: {
        id: string;
        email: string;
        first_name?: string;
        last_name?: string | null;
        is_password_reset_requested: boolean;
        is_suspended: boolean;
        organizations: {
          code: string;
          permissions: string | null;
          roles: string | null;
        }[];
        phone?: string | null;
        username?: string | null;
        image_url?: string | null;
      };
    };

    type KindeEvent = {
      type: string;
      data: KindeEventData;
    };

    const http = httpRouter();

    const handleKindeWebhook = httpAction(async (ctx, request) => {
      const event = await validateKindeRequest(request);
      if (!event) {
        return new Response("无效请求", { status: 400 });
      }

      switch (event.type) {
        case "user.created":
          await ctx.runMutation(internal.users.createUserKinde, {
            kindeId: event.data.user.id,
            email: event.data.user.email,
            username: event.data.user.first_name || ""
          });
          break;
        {/** 
        case "user.updated":
          const existingUserOnUpdate = await ctx.runQuery(
            internal.users.getUserKinde,
            { kindeId: event.data.user.id }
          );

          if (existingUserOnUpdate) {
            await ctx.runMutation(internal.users.updateUserKinde, {
              kindeId: event.data.user.id,
              email: event.data.user.email,
              username: event.data.user.first_name || ""
            });
          } else {
            console.warn(
              `未找到需要更新的用户,其 kindeId 为 ${event.data.user.id}.`
            );
          }
          break;
        */}
        case "user.deleted":
          const userToDelete = await ctx.runQuery(internal.users.getUserKinde, {
            kindeId: event.data.user.id,
          });

          if (userToDelete) {
            await ctx.runMutation(internal.users.deleteUserKinde, {
              kindeId: event.data.user.id,
            });
          } else {
            console.warn(
              `未找到需要删除的用户,其 kindeId 为 ${event.data.user.id}.`
            );
          }
          break;
        default:
          console.warn(`未处理的事件类型:${event.type}`);
      }

      return new Response(null, { status: 200 });
    });

    // ===== JWT 验证部分 =====
    async function validateKindeRequest(request: Request): Promise<KindeEvent | null> {
      try {
        if (request.headers.get("content-type") !== "application/jwt") {
          console.error("无效的 Content-Type。期望 application/jwt");
          return null;
        }

        const token = await request.text(); // JWT 作为纯文本发送在请求体中。
        const JWKS_URL = `${process.env.KINDE_ISSUER_URL}/.well-known/jwks.json`;
        const JWKS = createRemoteJWKSet(new URL(JWKS_URL));

        const { payload } = await jwtVerify(token, JWKS);

        // 确保负载包含预期属性
        if (
          typeof payload === "object" &&
          payload !== null &&
          "type" in payload &&
          "data" in payload
        ) {
          return {
            type: payload.type as string,
            data: payload.data as KindeEventData,
          };
        } else {
          console.error("负载结构不符合预期要求");
          return null;
        }
      } catch (error) {
        console.error("JWT 认证失败", error);
        return null;
      }
    }

    http.route({
      path: "/kinde",
      method: "POST",
      handler: handleKindeWebhook,
    });

    export default http;

点击全屏按钮进入全屏 点击退出按钮退出全屏

请参阅这篇帖子以获取有关在 Kinde 和 Convex 之间设置 webhook 的详细指南:参阅这篇帖子

  • 应用你的更改

运行命令 npx convex dev 以让您的配置自动同步到后端。

在终端中运行此命令: npx convex dev

进入全屏 退出全屏

  • 安装 Kindle 电子书阅读器

在新的终端窗口里,打开并安装 Kinde Next.js 库(或npm包)。

    npm install @kinde-oss/kinde-auth-nextjs

要安装这个包,请在终端中输入以上命令。@kinde-oss/kinde-auth-nextjs 是一个用于 Next.js 的 Kinde 认证插件。

点击全屏模式 点一下退出全屏

  • 复制一下你的 Kindle 设置.

在 Kinde 仪表盘上,点击你应用的 查看详细信息

复制 Kindle 的环境变量

往下滚动,然后复制你的 Client IDClient secret

请点击并复制Kinde中的客户端ID和密钥

  • 设置 Kinde 身份验证路由处理器

创建一个文件 app/api/auth/[kindeAuth]/route.ts 在你的 Next.js 项目里。在文件 route.ts 中复制以下代码:

    import {handleAuth} from "@kinde-oss/kinde-auth-nextjs/server";
    // 这里引入了身份验证处理函数,并设置了GET请求的处理方式
    export const GET = handleAuth();

全屏: 进入 全屏退出

这将处理你 Next.js 应用中的 Kinde 认证接口。

重要提示! Kinde SDK 依赖于该文件位于上述指定位置如下。

  • 为 Convex 和 Kinde 集成配置一个新的提供商

在你的项目根目录下创建一个 providers 文件夹,并添加一个新的文件 ConvexKindeProvider.tsx。这个 provider 会集成 Convex 和 Kinde,并包裹整个应用。

ConvexKindeProvider.tsx 文件中,将 ConvexProvider 包裹在 KindeProvider 中,并使用 useKindeAuth 获取认证令牌,然后把它传给 Convex。

domainclientIdredirectUri 传递给 KindeProvider

"use client";

import { ReactNode, useEffect } from "react";
import { KindeProvider, useKindeAuth } from "@kinde-oss/kinde-auth-nextjs";
import { ConvexProvider, ConvexReactClient, AuthTokenFetcher } from "convex/react";

const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL as string);

const ConvexKindeProvider = ({ children }: { children: ReactNode }) => {
  const { getToken } = useKindeAuth();

  useEffect(() => {
    const fetchToken: AuthTokenFetcher = async () => {
      const token = await getToken();
      return token || null;
    };

    if (typeof getToken === "function") {
      convex.setAuth(fetchToken);
    }
  }, [getToken]);

  return (
    <KindeProvider
      domain={process.env.NEXT_PUBLIC_KINDE_DOMAIN as string}
      clientId={process.env.NEXT_PUBLIC_KINDE_CLIENT_ID as string}
      redirectUri={process.env.NEXT_PUBLIC_KINDE_REDIRECT_URI as string}
    >
      <ConvexProvider client={convex}>{children}</ConvexProvider>
    </KindeProvider>
  );
};

export default ConvexKindeProvider;

全屏模式 (按 esc 退出)

将配置好的 ConvexKindeProvider.tsx 导入到主 layout.tsx 文件中。

    import type { Metadata } from "next";
    import "./globals.css";
    import ConvexKindeProvider from "@/providers/ConvexKindeProvider";

    export const metadata: Metadata = {
      title: "创建 Next 应用",
      description: "Kinde 和 Convex 演示",
    };

    export default function RootLayout({
      children,
    }: Readonly<{
      children: React.ReactNode;
    }>) {
      return (
        <ConvexKindeProvider>
          <html lang="en">
            <body>
              {children}
            </body>
          </html>
        </ConvexKindeProvider>
      );
    };

全屏;退出全屏

  • 基于认证状态显示界面

你可以使用来自"convex/react""@kinde-oss/kinde-auth-nextjs"的组件来控制显示哪个界面,这取决于用户是否已登录。

要开始,请创建一个允许用户登录和登出的shell。

因为 DisplayContent 组件是 Authenticated 的子组件,因此在其及其子组件内部,认证都是有保证的,Convex 查询也可以如此依赖。

    "use client";

    import { useKindeBrowserClient } from "@kinde-oss/kinde-auth-nextjs";
    import {
      RegisterLink,
      LoginLink,
      LogoutLink,
    } from "@kinde-oss/kinde-auth-nextjs/components";
    import { Authenticated, Unauthenticated, useQuery } from "convex/react";
    import { api } from "@/convex/_generated/api";

    function App() {
      const { isAuthenticated, getUser } = useKindeBrowserClient();
      const user = getUser();

      return (
        <main>
          <Unauthenticated>
            <LoginLink postLoginRedirectURL="/dashboard">点击登录</LoginLink>
            <RegisterLink postLoginRedirectURL="/welcome">点击注册</RegisterLink>
          </Unauthenticated>

          <Authenticated>
            {isAuthenticated && user ? (
              <div>
                <p>名: {user.given_name} {user.family_name}</p>
                <p>邮箱: {user.email}</p>
                <p>手机号码: {user.phone_number}</p>
              </div>
            ) : null}

            <DisplayContent />
            <LogoutLink>点击登出</LogoutLink>
          </Authenticated>
        </main>
      );
    }

    function DisplayContent() {
      const { user } = useKindeBrowserClient();
      const files = useQuery(api.files.getForCurrentUser, {
        kindeId: user?.id,
      });

      return <div>认证内容: {files?.length}</div>;
    }

    export default App;

全屏 退出全屏

  • 在你的 Convex 函数中使用身份验证状态

如果用户通过身份验证,你可以通过 ctx.auth.getUserIdentity 获取 Kinde 发送的 JWT 中的用户信息。

如果用户未通过身份验证,ctx.auth.getUserIdentity 将返回 null

确保调用此查询的组件是Authenticated的子组件,该Authenticated来自"convex/react",否则会在页面加载时抛出异常。

    import { query } from "./_generated/server";

    export const getForCurrentUser = query({
      args: { kindeId: v.string() },
      handler: async (ctx, args) => {
        const identity = await ctx.auth.getUserIdentity();
        if (identity === null) {
          throw new Error("未通过身份验证");
        }
        const files = await ctx.db
          .query("files")
          .filter((q) => q.eq(q.field("kindeId"), args.kindeId))
          .collect();
        if (!files) {
          throw new Error("未找到此用户的任何文件");
        }
        return files;
      },
    });

全屏 退出全屏

登录和退出流程

现在你设置好了,你可以使用LoginLink组件来实现你的应用的登录功能。

如果您更喜欢为您的应用自定义登录或注册表单,参阅这篇帖子

    import {LoginLink} from "@kinde-oss/kinde-auth-nextjs/components";

    <LoginLink>登录</LoginLink>

全屏模式(进入/退出)

你可以使用 LogoutLink 组件,让用户轻松地注销。

    import {LogoutLink} from "@kinde-oss/kinde-auth-nextjs/components";

    <LogoutLink>登出链接</LogoutLink>

切换到全屏 / 退出全屏

登录和未登录时的视图

使用useConvexAuth()钩子而不是Kinde的useKindeBrowserClient钩子来检查用户是否已登录与否。useConvexAuth钩子确保浏览器获取了用于向Convex后端发送身份验证请求的认证令牌,并且该令牌已经被Convex后端验证。

    import { useConvexAuth } from "convex/react";

    function App() {
      const { isLoading, isAuthenticated } = useConvexAuth();

      return (
        <div className="App">
          {isAuthenticated ? "已登录状态" : "未登录或正在加载中"}
        </div>
      );
    }

全屏 开启 全屏 关闭

功能中的用户信息

请参阅 函数中的认证 以了解如何访问已验证用户的信息,在查询、突变和操作中。

参考在Convex的数据库中存储用户,了解如何在数据库中存储用户信息。

Next.js 中的用户信息管理

您可以从 Kinde 的 useKindeBrowserClientgetKindeServerSession 钩子函数获取经过身份验证的用户的名字和邮箱地址等信息。有关可用字段的列表,请参阅用户信息对象

    "use client";

    import { useKindeBrowserClient } from "@kinde-oss/kinde-auth-nextjs";

    export default function Hero() {
      const { user } = useKindeBrowserClient();
      return <span>您是 {user?.given_name}, {user?.family_name}</span>;
    };

全屏 退出全屏

配置开发和生产环境

让你的 Kinde 实例在 Convex 开发和生产部署之间有所不同,可以通过在 Convex 仪表盘上配置环境变量来实现这一点。

配置后端系统

Kinde 的默认配置设置为生产环境。若要使用自定义域名,请参阅此指南[在此处](https://docs.kinde.com/build/domains/pointing-your-domain/#:~:text=转到设置 > 环境 > 自定义域名,&text=在弹出窗口中,输入您的域名 <your_app>.kinde.com,然后点击保存)。

开发设置

打开你开发部署的设置,在Convex 仪表板中将你 .env.local 文件中的所有变量添加进去。

带有环境中变量的凸形仪表盘

生产环境的设置

同样,在突出的仪表板上,在左侧菜单中切换到生产部署,并在你的 .env.local 文件里设置相应的变量。

现在运行命令 npx convex deploy 来切换到新的配置。

npx convex 部署命令

进入全屏,退出全屏

发布你的 Next.js 应用

根据您所使用的托管平台,在生产环境中设置相应的环境变量。参见托管平台以获取更多信息。

调试身份验证

如果用户成功完成了 Kinde 的注册或登录过程,并且在被保存到您的 Convex 数据库之后,页面会重定向回用户,但 useConvexAuth 却返回 isAuthenticated: false,那么您的后端可能没有正确配置。

你的 convex/ 目录下的 auth.config.ts 文件列出了已配置的身份验证提供程序。在添加新的提供程序后,你必须运行 npx convex devnpx convex deploy 来将配置同步到后端服务器。

对于更详细的调试方法,请参阅身份验证调试

技术细节

实际上,认证流程内部是这样的。

  1. 用户点注册或登录按钮。
  2. 用户会被引导到 Kinde 的页面,在那里他们可以通过您设定的方法注册或登录。
  3. 接着,他们的信息将通过 webhook 发送到 Convex 并被安全存储,然后用户会被立即重定向回您的页面或您通过 Kinde 的 postLoginRedirectURL 属性设置的其他页面。
  4. KindeProvider 现在知道了用户已通过身份验证。
  5. useKindeAuthAuthTokenFetcher 从 Kinde 获取一个授权令牌。
  6. 然后 react 的 useEffect 钩子将其设置为 Convex 的 setAuth 实例。
  7. ConvexProvider 将其传递给 Convex 后端验证。
  8. 您的 Convex 后端会从 Kinde 获取 domain、clientId 和 redirectUri,以验证令牌签名的有效。
  9. ConvexProvider 收到身份验证成功的通知,现在您的整个应用程序都知道用户已通过 Convex 进行身份验证。useConvexAuth 返回 isAuthenticated: true,并且 Authenticated 组件会渲染其子组件。

ConvexKindeProvider.tsx 文件中的设置负责在需要时刷新令牌,以确保用户始终与后端保持认证状态。

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消