使用Clerk的“组织”功能和Next.js构建多租户应用
构建一个功能齐全的多租户应用程序可能非常有挑战性。除了需要一个灵活的注册和登录功能之外,例如,你还需要实现几个其他必要的部分。
- 创建及管理租户
- 用户邀请操作流程
- 管理角色和权限
- 在整个应用中实施数据隔离和访问控制
这听起来确实是个大工程。如果你是个资深的SaaS开发者,你可能已经不止一次地做过了。
Clerk 是最受欢迎的身份验证和用户管理云服务之一,也是。它结合了 API 和预构建的 UI 组件,极大地简化了这些功能在应用程序中的集成过程。同样,其较新的“组织”功能为构建多租户应用程序时提供了一个很好的起点。在这篇文章中,我们将探讨如何利用它来构建一个有一定复杂度的应用程序,并且尽量保持代码简洁和清晰。
目标与架构我们将要构建的目标应用程序是一个待办事项清单。它的核心功能虽然简单:创建列表并管理里面的待办事项。不过,重点将放在多租户支持和访问控制方面,
- 组织管理
用户可以创建组织并邀请他人加入。他们可以管理成员并分配角色。 - 当前上下文
用户可以选择一个组织作为当前上下文。 - 数据隔离
只能看到当前组织内的数据。 - 基于角色的访问控制
- 管理员成员可以访问其组织内的所有数据。
- 普通成员可以访问他们自己拥有的待办事项列表。
- 普通成员可以查看其他成员的待办事项列表并管理这些列表的内容,除非该列表被设置为私有。
Clerk 可以与任何 JavaScript 框架一起使用,但其对 Next.js 的支持尤其适合。因此,我们将使用 Next.js 作为我们的全栈框架之一,并且还会用到两个重要的工具。
你可以在这个帖子的结尾找到项目链接。
添加组织管理来添加组织管理我假设你已经创建了一个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 点赞
评论
共同学习,写下你的评论
评论加载中...
作者其他优质文章
正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦