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

使用Clerk的“组织”功能和Next.js构建多租户应用

构建一个功能齐全的多租户应用程序可能非常有挑战性。除了需要一个灵活的注册和登录功能之外,例如,你还需要实现几个其他必要的部分。

  • 创建及管理租户
  • 用户邀请操作流程
  • 管理角色和权限
  • 在整个应用中实施数据隔离和访问控制

这听起来确实是个大工程。如果你是个资深的SaaS开发者,你可能已经不止一次地做过了。

Clerk 是最受欢迎的身份验证和用户管理云服务之一,也是。它结合了 API 和预构建的 UI 组件,极大地简化了这些功能在应用程序中的集成过程。同样,其较新的“组织”功能为构建多租户应用程序时提供了一个很好的起点。在这篇文章中,我们将探讨如何利用它来构建一个有一定复杂度的应用程序,并且尽量保持代码简洁和清晰。

目标与架构

我们将要构建的目标应用程序是一个待办事项清单。它的核心功能虽然简单:创建列表并管理里面的待办事项。不过,重点将放在多租户支持和访问控制方面,

  • 组织管理
    用户可以创建组织并邀请他人加入。他们可以管理成员并分配角色。
  • 当前上下文
    用户可以选择一个组织作为当前上下文。
  • 数据隔离
    只能看到当前组织内的数据。
  • 基于角色的访问控制
    - 管理员成员可以访问其组织内的所有数据。
    - 普通成员可以访问他们自己拥有的待办事项列表。
    - 普通成员可以查看其他成员的待办事项列表并管理这些列表的内容,除非该列表被设置为私有。

Clerk 可以与任何 JavaScript 框架一起使用,但其对 Next.js 的支持尤其适合。因此,我们将使用 Next.js 作为我们的全栈框架之一,并且还会用到两个重要的工具。

  • Prisma: 对象关系映射 (ORM)
  • ZenStack: 构建在 Prisma 之上的访问控制层

你可以在这个帖子的结尾找到项目链接。

添加组织管理来添加组织管理

我假设你已经创建了一个Next.js项目,并且已经按照此指南设置了Clerk的基本注册和登录流程。也请确保你已经在Clerk的仪表板中开启了“组织”功能参见这里

现在,我们可以将“OrganizationSwitcher”模块在布局中添加了。

// src/app/layout.tsx

导入 { OrganizationSwitcher }@clerk/nextjs

...

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <ClerkProvider>
      <html lang="en">
        <body>
          <header>
            <SignedOut>
              <SignInButton />
            </SignedOut>
            <SignedIn>
              <div>
                {/* 显示组织切换器和用户按钮 */}
                <OrganizationSwitcher />
                <UserButton />
              </div>
            </SignedIn>
          </header>
        </body>
      </html>
    </ClerkProvider>
  );
}

有了这句话设定,你就能用上一组功能齐全的UI组件,用于管理组织和选择激活的组织!

![](https://imgapi.imooc.com/676a0fa5091e325d08780730.jpg)

# 设置数据库环境[](https://zenstack.dev/blog/clerk-multitenancy#setting-up-the-database)

我们的用户和组织数据存储在 Clerk 的服务器上。我们需要将待办事项列表和条目存储在我们自己的数据库中。在本节中,我们将设置 Prisma 和 ZenStack,并创建数据库架构。

让我们从安装所需的软件包开始:

在命令行中输入以下命令来安装依赖项:

npm install --save-dev prisma zenstack
npm install @prisma/client @zenstackhq/runtime


然后我们可以创建数据库模式设计。请注意,我们将创建一个 schema.zmodel 文件(替换原有的 "schema.prisma" 文件)。[ZModel 语言](https://zenstack.dev/docs/the-complete-guide/part1/zmodel) 具有 Prisma 模式语言的超集特性,不仅可以用来建模数据模式,还可以用来定义访问控制策略。在这部分,我们只专注于数据建模。
// schema.zmodel

数据源 db {  
  提供程序 = "postgresql"  
  url      = env("DATABASE_URL")  
}  

生成器 JavaScript {  
  提供程序 = "prisma-client-js"  
}  

// 任务列表  
模型 List {  
  id        String        @id @default(cuid())  
  创建时间 DateTime      @default(now())  
  title     String  
  private   Boolean       @default(false)  
  orgId     String?  
  ownerId   String  
  todos     Todo[]  
}  

// 任务条目  
模型 Todo {  
  id          String    @id @default(cuid())  
  title       String  
  完成时间 DateTime?  
  list        List      @relation(fields: [listId], references: [id], onDelete: Cascade)  
  listId      String  
}

你可以生成一个标准的 Prisma 模式文件,并将模式推送到数据库中:
# 运行 `zenstack generate` 会生成 "prisma/schema.prisma" 文件
# 并运行 `prisma generate`
npx zenstack generate
npx prisma db push

最后一步,创建一个“src/server/db.ts”文件来导出 Prisma 客户端部分:
// src/server/db.ts    

import { PrismaClient } from "@prisma/client";  
export const prisma = new PrismaClient();

# 实现访问控制功能:[​](https://zenstack.dev/blog/clerk-multitenancy#implementing-access-control)

如前所述,ZenStack 允许你同时建模数据和访问控制在单一模式中。让我们看看如何完全利用它来实现我们的授权需求。规则可以通过 `@@allow` 和 `@@deny` 属性来指定。默认情况下,访问会被拒绝,除非通过明确的 `@@allow` 规则授予。

虽然授权和认证是不同的概念,但授权通常依赖于认证来生效。例如,要确定当前用户是否有权限访问一个列表,需要基于用户的ID、当前组织及用户在组织中的角色来判断。要获取这些信息,我们先定义一个类型来表示这些信息。
// schema.zmodel

// 定义 `auth()` 
type Auth {
  // 用户ID
  userId         String  @id

  // 当前组织ID
  currentOrgId   String?

  // 当前组织职位
  currentOrgRole Role?

  @@auth
}

然后你可以使用访问策略规则中的特殊 `auth()` 函数来访问当前用户的资料。我们以 `List` 模型为例来说明这些规则是如何设定的。规则的设定方式如下所示。
// schema.zmodel  

model List {  
  ...  

  // 拒绝匿名访问  
  @@deny('all', auth() == null)  

  // 租户隔离:如果用户的当前组织ID不匹配,则拒绝访问  
  @@deny('all', auth().currentOrgId != orgId)  

  // 所有者或管理员具有全部访问权限  
  @@allow('all', auth().userId == ownerId || auth().currentOrgRole == 'org:admin')  

  // 如果不是私有的,组织成员可以读取  
  @@allow('read', !private)  

  // 在创建时,所有者必须设为当前用户  
  @@allow('create', ownerId == auth().userId)  
}

最后一个谜题是,你可能已经猜到了,`auth()` 的值从何而来?在运行时,ZenStack 提供了一个 `enhance()` API 用来生成一个增强版的 `PrismaClient`(一个轻量级的包装器),该包装器会自动执行访问策略。当你调用 `enhance()` 时,传入一个从身份验证提供方获取的用户上下文,这个上下文中的信息会用来决定 `auth()` 的值。

我们下一节再仔细看看它怎么运作。

# 最后,用户界面 (UI)

在我们开始创建UI之前,我们先做一个辅助工具,来获取增强的`PrismaClient`,适用于当前用户。
// src/server/db.ts  

import { auth } from "@clerk/nextjs/server";  
import { Role } from "@prisma/client";  
import { enhance } from "@zenstackhq/runtime";  

export async function getUserDb() {  
  // 从 Clerk 获取当前用户的信息  
  const { userId, orgId, orgRole } = await auth();  

  // 创建一个带有正确用户上下文的增强 Prisma 客户端  
  const user = userId  
    ? {  
        userId,  
        currentOrgId: orgId,  
        currentOrgRole: orgRole  
      }  
    : undefined; // 匿名  
  return enhance(prisma, { user });  
}

让我们使用[React Server Components](https://nextjs.org/docs/app/building-your-application/rendering/server-components)(RSC)和[Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations)来构建用户界面。我们将统一使用`getUserDb()`助手来访问数据库,确保访问控制。

下面的RSC用于为当前用户显示待办事项列表(省略了样式)如下:
// src/components/TodoList.tsx  

// 显示当前用户的待办事项列表(TodoList)组件  

export default async function TodoLists() {  
  const db = await getUserDb();  

  // 增强的 PrismaClient 会自动过滤用户无法访问的列表  
  const lists = await db.list.findMany({  
    orderBy: { updatedAt: "desc" },  
  });  

  return (  
    <div>  
      <div>  
        {/* 要创建新列表的客户端组件 */}  
        <CreateList />  
        <ul>  
          {lists?.map((list) => (  
            <Link href={`/lists/${list.id}`} key={list.id}>  
              <li>{list.title}</li>  
            </Link>  
          ))}  
        </ul>  
      </div>  
    </div>  
  );  
}

一个客户端组件通过调用服务器上的方法来创建一个新列表。
// src/components/CreateList.tsx

"use client";

import { createList } from "~/app/actions";

export default function CreateList() {
  function onCreate() {
    const title = prompt("请输入列表的标题");
    if (title) {
      createList(title);
    }
  }
  return (
    <button onClick={onCreate}>
      创建列表
    </button>
  );
}
// src/app/actions.ts  

'use server';  

import { revalidatePath } from "next/cache";  
import { getUserDb } from "~/server/db";  

export async function 新建列表(title: string) {  
  const db = await getUserDb();  
  await db.list.create({ data: { title } });  
  revalidatePath("/");  
}


![](https://imgapi.imooc.com/676a0fa60a8dc8e506000553.jpg)

管理待办事项的组件详情此处未展示,但思路大同小异。完整代码及未展示的组件详情请参见此处。

# 总结 [](https://zenstack.dev/blog/clerk-multitenancy#结论)

认证和授权是大多数应用程序的两大基石。对于多租户系统来说,构建这些功能特别具有挑战性。本文将展示如何通过结合 Clerk 提供的“组织”功能以及 ZenStack 的访问控制功能,简化和优化这些工作。最终结果是一个既安全又灵活的简洁应用程序,无需编写大量样板代码。

clerk 还支持为组织定义 [自定义角色和权限](https://clerk.com/docs/organizations/roles-permissions)(还在测试阶段)。虽然这篇文章没有详细讨论这个功能,但通过一些调整,你就可以利用它来定义访问策略。这样一来,你就可以通过 Clerk 的仪表板来管理权限,并让 ZenStack 在运行时来执行这些权限。

[ZenStack](https://zenstack.dev/) 是我们的开源 TypeScript 工具包,用于更快、更智能且更快乐地构建高质量、可扩展的应用程序。它将数据模型、访问策略和验证规则集中在一个基于 Prisma 的声明式模式中,非常适合用于 AI 增强的开发环境。现在就将 [ZenStack](https://zenstack.dev/) 与您的现有栈集成吧!
点击查看更多内容
TA 点赞

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

评论

作者其他优质文章

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

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消