如何在 10 分钟内用 100 行代码构建可扩展的 SaaS 后端
构建可扩展的 SaaS 系统很困难
构建可扩展的 SaaS 系统很困难
在我之前的公司参与过四款商业 SaaS 产品的开发之后,我意识到与典型的消费产品相比,这类产品存在诸多复杂性。在这些复杂性中,一个突出的领域是权限控制和访问策略的复杂性。
除了单个用户的权限管理之外,还需要在组织层面进行权限管理,更不用说用户容易忽略的租户隔离这一关键环节。无论您选择 RBAC 还是 ABAC 访问控制机制,系统的复杂性都会随着功能的增加而增加。因此,添加新功能所需的时间会逐渐减慢,类似于 Martin Fowler 的 “设计耐力假说”中蓝线所示的下降趋势:
时间并非最坏的因素,因为我亲眼目睹过一些案例,添加新功能可能会破坏现有功能。这在 B2B SaaS 领域尤其成问题,这类问题可能会造成致命的后果。
访问控制代码分散
造成这一挑战的一个突出因素是代码库中访问控制处理的分散性。一些访问控制逻辑集中在特定组件中,例如中间件或数据库的 RLS(行级安全性),而其他访问控制逻辑则在各个组件中实现。因此,即使是看似简单的更改,也需要对整个代码库及其复杂性有深入的理解。
开发人员不仅必须小心不要无意中破坏现有功能,还必须确定实施更改的最佳位置,而不会引入潜在问题。
如果有一种方法可以将所有访问控制逻辑整合到一个集中位置,那会怎样?
单一事实来源
ZenStack来了:一个基于Prisma ORM 构建的 Typescirpt 工具包。它使用 Prisma 之上的声明式数据模型,添加访问策略和验证规则,并自动为您生成 RESTful 或 tRPC API。
使用 ZenStack 的 SaaS 项目
以下是您可以开始的 SaaS 后端项目:
https://github.com/zenstackhq/saas-backend-template
特征
- 多租户
- 软删除
- 群组共享
数据模型
其中schema.zmodel,
有4个模型,它们之间的关系如下:
- 组织 (Organization) 是顶级租户。任何用户 (User)、帖子 (Post) 和群组 (Group) 的实例都属于某个组织。
- 一个用户可以属于多个组织和组
- 一个帖子属于一个用户,但可能属于多个群组。
权限
让我们看一下帖子的所有权限以及如何使用 ZenStack 的访问策略来表达它们。
💡您可以在下面找到访问策略语法的详细参考:
https://zenstack.dev/docs/reference/zmodel-language#access-policy
- 创造
所有者必须设置为当前用户,组织必须设置为当前用户所属的组织。
@@allow('create', owner == auth() && org.members?[id == auth().id])
-
更新
只有所有者可以更新它,并且不允许更改组织或所有者
@@allow('update', owner == auth() && org.future().members?[id == auth().id] && future().owner == owner)
-
读
-
允许所有者阅读
@@allow('read', owner == auth())
-
如果它是公开的,则允许组织成员阅读
@@allow('read', isPublic && org.members?[id == auth().id])
-
允许小组成员阅读
@@allow('read', groups?[users?[id == auth().id]])
-
-
删除
- 不允许删除如果没有指定规则,则默认不允许该操作。
-
如果为真,则该记录被视为已删除
isDeleted
,即软删除。@@deny('all', isDeleted == true)
您可以在以下位置查看完整的数据模型以及上述访问策略schema.zmodel
:
abstract model organizationBaseEntity {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
isDeleted Boolean @default(false) @omit
isPublic Boolean @default(false)
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
ownerId String
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
orgId String
groups Group[]
// when create, owner must be set to current user, and user must be in the organization
@@allow('create', owner == auth() && org.members?[id == auth().id])
// only the owner can update it and is not allowed to change the owner
@@allow('update', owner == auth() && org.members?[id == auth().id] && future().owner == owner)
// allow owner to read
@@allow('read', owner == auth())
// allow shared group members to read it
@@allow('read', groups?[users?[id == auth().id]])
// allow organization to access if public
@@allow('read', isPublic && org.members?[id == auth().id])
// can not be read if deleted
@@deny('all', isDeleted == true)
}
model Post extends organizationBaseEntity {
title String
content String
}
模型继承
您可能好奇,为什么这些规则是在抽象organizationBaseEntity
模型中定义,而不是在具体Post
模型中定义。这就是为什么我说它是可扩展的。借助 ZenStack 的模型继承功能,所有常见的权限都可以在抽象基础模型中方便地处理。
考虑一下这样一个场景:一个新雇佣的开发人员需要添加一个新ToDo
模型。他可以通过简单地扩展以下内容轻松实现这一点organizationBaseEntity
:
model ToDo extends organizationBaseEntity {
name String
isCompleted Boolean @default(false)
}
所有多租户、软删除和共享功能都将自动运行。此外,如果需要任何专门的访问控制逻辑ToDo
,例如允许共享用户更新,您可以轻松在ToDo
模型中添加相应的策略规则,而不必担心破坏现有功能:
@@allow('update', groups?[users?[id == auth().id]] )
我需要编写多少 NodeJS 代码
至此,您已经将架构作为业务模型和访问控制的唯一真实来源。接下来,您可能希望看到需要编写的 Typescript/JavaScript 代码。接下来是激动人心的部分:
就这样。您几乎不需要编写任何 TS/JS 代码。ZenStack 会自动为您生成 API,并在运行时无缝注入访问控制。
虽然您可能会看到中的几行代码index.ts
,但它主要用于将自动生成的 RESTful API 安装到 Express.js 中并启动应用程序。
💡它支持大多数 Node.js 框架,如 Next.js、Sveletkit、Fastify 等。
让我们玩一玩
我创建了示例数据供您使用。您可以运行以下命令来为其设定种子:
npm run seed
数据如下:
因此在 Prisma 团队中,每个用户都创建了一个帖子:
- 加入 Discord未共享,因此只有 Robin 可以看到
- 加入 Slack在 Robin 所属的群组中共享,以便 Robin 和 Bryan 都可以看到。
- 关注 Twitter是公开的,以便 Robin、Bryan 和 Gavin 可以看到
您可以简单地调用 Post 端点来查看模拟不同用户的结果:
curl -H "X-USER-ID: robin@prisma.io" localhost:3000/api/post
💡为了方便测试,这里使用了纯文本形式的用户 ID。实际应用中,你应该使用更安全的方式传递 ID,例如 JWT 令牌。
根据样本数据,每个用户应该看到不同的帖子数量,从 0 到 3。
软删除
由于是软删除,实际操作是更新isDeleted
为 true。我们来删除 Robin 的“加入 Salck”帖子,运行以下命令:
curl -X PUT \
-H "X-USER-ID: robin@prisma.io" -H "Content-Type: application/json" \
-d '{"data":{ "type":"post", "attributes":{ "isDeleted": true } } }'\
localhost:3000/api/post/slack
此后,如果您再次尝试访问 Post 端点,结果将不再包含“加入 Slack”的帖子。如果您对钩子下的工作原理感兴趣,请查看另一篇文章:
最后的
SaaS 产品MermaidChart近期推出了由 ZenStack 提供支持的 Teams 功能。如果您想了解他们采用 ZenStack 的经验,欢迎加入我们的Discord。更多相关信息和示例,请访问我们的官方网站。
你能幫我嗎?
如果您觉得 ZenStack 能帮到您,请给我一颗星,我会非常高兴,这样它就能真正帮助更多人快速行动!❤️
https://github.com/zenstackhq/zenstack
继续阅读:https://dev.to/zenstack/how-to-build-a-scalable-saas-backend-in-10-minutes-with-100-lines-of-code-using-zenstack-1h7c