项目四 多租户 SaaS 基础版
用共享 Worker、D1 租户边界、配额与可选客户域名完成一个最小 SaaS 闭环。
编辑与核验:橙宝书编辑团队 ·
完成标准
用户能加入一个租户并创建记录;另一租户无法读取或修改该记录;写入受认证与配额保护;客户域名和客户代码执行被明确标记为后续能力,不阻塞基础版上线。
前置条件与运行随书示例
需要 Node.js 22、pnpm 和已安装的仓库依赖。示例位于 examples/multitenant-saas,只使用本地 D1;先复制本地 Secret、应用 migration,再写入两组演示成员关系:
cp examples/multitenant-saas/.dev.vars.example examples/multitenant-saas/.dev.vars
pnpm wrangler d1 migrations apply orange-book-saas --local --config examples/multitenant-saas/wrangler.jsonc
pnpm wrangler d1 execute orange-book-saas --local --file examples/multitenant-saas/seed.local.sql --config examples/multitenant-saas/wrangler.jsonc
pnpm wrangler dev --config examples/multitenant-saas/wrangler.jsonc另一个终端运行 node --test examples/multitenant-saas/test/index.test.mjs,应看到 5 个测试通过。随后按示例 README 的 curl 创建项目,响应状态应为 201,JSON 中的 tenantId 为 tenant-a。把请求改成不存在的成员关系应得到 403,超过 PROJECT_LIMIT 应得到 409 project_quota_reached。
示例认证边界
API_KEY 与 x-user-id 只用于演示服务端成员查询。公开 SaaS 必须换成真实会话或签名身份;不能把共享 Key 放进浏览器代码。
先选择正确的多租户模型
普通 SaaS 不需要从 Workers for Platforms 开始。基础版使用一个共享 Worker 执行业务代码,在可信认证结果中取得 tenantId,并在每个 D1 查询中绑定租户条件。只有当平台需要运行客户提交或 AI 生成的不可信代码,并要求每客户独立 Worker、CPU/子请求限制和 dispatch namespace 时,再评估 Workers for Platforms。
| 需求 | 起步方案 | 升级条件 |
|---|---|---|
| 多账号共享一套业务代码 | Worker + D1 tenant 列 | 数据规模或合规要求需要更强隔离 |
| 每租户子域名 | 通配 DNS +可信 hostname 映射 | 需要客户自有域名 |
| 客户自有域名 | Cloudflare for SaaS custom hostname | 需要自动化证书/域名生命周期 |
| 运行客户或 AI 生成代码 | 不放进共享 Worker | Workers for Platforms 隔离执行 |
最小数据边界
每张租户业务表都包含 tenant_id,唯一约束和索引也要考虑租户。认证层从会话或签名 Token 得到用户,再从成员关系得到租户;浏览器提交的 x-tenant-id 只能是选择提示,不能单独证明权限。
const { userId, tenantId } = await requireMembership(request, env);
const result = await env.DB.prepare(
`SELECT id, name, created_at
FROM projects
WHERE tenant_id = ?1 AND id = ?2`
).bind(tenantId, projectId).first();读取、更新与删除都包含 tenant_id。返回 404 比“记录存在但你无权访问”更不容易泄露其他租户对象是否存在。真实认证实现取决于项目,不要把示例里的 helper 当成已存在接口。
建立核心闭环
创建组织与成员关系
建立 tenants、users、memberships 与一张业务表。邀请和加入操作需要一次性 token、过期时间和幂等处理。
保护每个写操作
Worker 先验证会话,再验证成员角色和 tenant 条件。写入使用 prepared statement;body、字符串、列表和上传都有明确上限。
记录业务配额
套餐配额保存在可信数据中,并在写操作前后以可审计方式更新。WAF Rate Limiting 保护网络滥用,但不能代替精确的租户计费或额度。
验证跨租户失败
创建租户 A 和 B。用 A 创建记录,再用 B 的身份读取、修改和删除,全部应返回稳定拒绝结果。日志记录 request ID 与内部原因,不回显其他租户数据。
客户域名边界
Cloudflare for SaaS 可把客户 custom hostname 接到你的平台,并管理对应证书与路由。生产就绪不能只看 CNAME:还要检查 hostname 与 SSL 状态、验证方式、fallback origin 和失败重试。若暂时只需要 tenant.example.com,先完成自有域下的子域名映射,再单独规划客户域名功能。
域名不是 tenant 身份
hostname 只用于路由到候选租户。服务端仍要把 hostname 映射到可信 tenant 记录,并对登录用户执行成员授权,不能把任意 Host header 直接当作租户 ID。
安全、成本与可观测
Secret
成本
日志
滥用
验证与排障
| 现象 | 先检查 | 恢复动作 |
|---|---|---|
401 unauthorized | .dev.vars 与 x-api-key 是否一致 | 重新复制本地模板,不把值提交到仓库 |
403 forbidden | memberships 是否同时存在 user 与 tenant | 只在本地执行 seed,生产通过受审计邀请流程建成员 |
409 project_quota_reached | PROJECT_LIMIT 与该 tenant 的记录数 | 清理演示记录或调整可信套餐配置 |
D1_ERROR | local migration 是否已应用、binding 是否名为 DB | 重新应用本地 migration,再重启 Wrangler |
远程边界与回滚
创建 D1、应用远程 migration、配置 custom hostnames、启用 Workers for Platforms 或部署生产规则都会修改 Cloudflare 状态,本教程不自动执行。代码回滚不能删除新 Schema 或恢复已变更的客户域名。先使用兼容 migration,保留 hostname 状态表和失败重试记录;出现越权风险时优先停写并禁用相关路由,再恢复稳定代码并审计受影响租户。
下一步:建立网站与应用防御基线。
官方来源
这篇内容帮你完成目标了吗?
内测反馈只在当前浏览器生成,不会自动上传。