安全与风控
在 Cloudflare 或 Vercel…
当需要隔离租户数据、支持自定义域名、构建白标平台或评估 PSL 注册时,按顺序完成平台选型、域名策略、隔离模型、路由、上下文传播、绑定、自定义域名与限额映射。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
在 Cloudflare 或 Verce特定
解决思路
- 先做平台分派:若租户上传或生成自己的代码、需要代码级隔离、在 KV/D1/Durable Objects/R2 上做边缘计算,选 Cloudflare,模型为在 dispatch namespace 前放一个 Dispatch Worker,每个租户一个 Worker,自定义主机名用 Cloudflare for SaaS;若所有租户运行同一套 Next.js 代码、仅内容/品牌/套餐不同,且使用 ISR、Server Components、Vercel 部署,选 Vercel,模型为单一部署,用 proxy.ts 从 hostname 解析租户,项目上配置通配符加自定义域名。
- 每个产品只选一个平台;不要用 Cloudflare 代理挡在 Vercel 应用前面,否则 TLS 和重定向层翻倍,通常导致重定向循环和证书签发失败。
- 若租户要在 Vercel 上发布自己的代码,改用多项目模型(每个租户一个 Vercel 项目,用 SDK 创建),其隔离思路同 Cloudflare 那一行;本技能的 Vercel 参考只覆盖单部署模型。
- 第 1 步:选择域名策略。把租户工作负载放在专用可注册域名上(租户用 acme.app,品牌用 acme.com);一个钓鱼租户在 x.acme.com 上会让整个域名进黑名单,且 Domain=acme.com 的租户 cookie 会触达你的控制台。
- 把控制台和认证放在与租户子域不同的 apex 上(控制台用 app.acme.com,租户用 *.acme.app)。
- 若租户在兄弟子域上发布内容或运行代码,把租户名正上方的标签(acme.app,或 <tenant>.sites.acme.app 对应的 sites.acme.app)提交到 PSL,并立即开始,因为没有 SLA;租户自有自定义域名无需 PSL 条目;否则记录 No PSL 及原因。
- 第 2 步:选择租户识别方式(选一个主方式,自定义域名作为升级路径)。子域 tenant.acme.app:通配符 DNS 加通配符证书,默认方案。自定义域名 tenant.com:租户 CNAME 到你,适用于付费租户,声誉转移到他们身上,需要第 7 步的入驻生命周期。路径 acme.app/tenant:无需每租户 DNS 或证书,但没有 cookie 隔离和品牌,仅在租户永远不会有主机名时选用。
- 第 3 步:定义隔离模型。计算侧 Cloudflare:一个 untrusted 模式的 dispatch namespace,按套餐设置每次调用的 cpuMs 和 subRequests 限制,若租户代码可能访问互联网则加一个 outbound Worker。计算侧 Vercel:单一部署,租户代码永不执行;若租户必须发布代码,改用 Vercel 多项目或 Cloudflare,而不是在应用内做沙箱。数据侧:默认共享 schema,每个租户相关表带 tenant_id 并启用 RLS;受监管或吵闹的租户用 database-per-tenant,可按套餐选择。
- 第 4 步:确定性路由(租户不能影响路由,也不能看到彼此)。Cloudflare:在 SaaS zone 上用单条 */* 路由到 dispatch Worker;hostname -> 租户记录(KV,未命中查 D1)-> env.DISPATCHER.get(script);Worker not found -> 404。Vercel:proxy.ts(Next.js 16;15 上用 middleware.ts 加 runtime: 'nodejs')读取 host,在 Global Config 或数据库中查租户,重写到租户段;未知主机名 -> 404,绝不回退到品牌站。
- 在任何租户重写之前放行 /.well-known;把 robots.txt、sitemap.xml、llms.txt 路由进租户段,使其按租户变化。
- 第 5 步:从单一权威传播租户上下文。删除所有入站 x-tenant-* 头,从解析出的租户设置 x-tenant-id、x-tenant-slug、x-tenant-plan,并在请求上转发(NextResponse.next({ request: { headers } }));Server Components 读 await headers(),route handlers 读 request.headers;Cloudflare 中由 dispatch Worker 在 fetch 前设置头或传参。
- 代理只负责路由,不负责授权;Server Functions、route handlers 和任务要从会话重新推导租户,数据层强制执行(RLS 或 tenant_id 谓词)。
- 第 6 步:只绑定租户需要的东西。Cloudflare:每个用户 Worker 有自己的绑定(KV namespace、D1 database、R2 prefix),新增绑定是显式重新部署,不用共享全局。Vercel:Global Config 只存 hostname -> { id, slug, plan },数据库是事实来源,域名验证时写穿;功能开关和品牌从数据库按租户 id 读取。
- 第 7 步:支持自定义域名和每租户静态文件。要设计和记录的生命周期:添加域名 -> 显示 DNS 目标 -> 验证所有权 -> 签发证书 -> 激活映射 -> 移除或失败路径。
- Cloudflare:在 SaaS zone 上用 Cloudflare for SaaS custom hostname,配置代理回退源,CNAME 目标为 customers.<you>.com,用 http 或 txt 验证,在 DNS 切换前预验证。
- Vercel:projectsAddProjectDomain -> 从项目域名卡片获取 DNS 值 -> 仅当域名已在 Vercel 上时才加 _vercel TXT -> projectsVerifyProjectDomain -> Let's Encrypt HTTP-01。
- robots.txt、sitemap.xml、llms.txt 是租户段内的 route handlers,带显式 Content-Type;任何租户相关内容都不放在 /public;其内容属于 seo 范畴。
- 第 8 步:把限额呈现为套餐并采集证据。从 limits-and-quotas.md 填限额到套餐表,重新核对每个来源 URL 并标注日期;在路由层执行(Cloudflare limits,Vercel 套餐头加服务端检查)。
- 请求路径中不要放长时间运行的工作:用 Cloudflare Queues 或 Workflows,Vercel background functions 或 cron。
- 每个租户操作(创建租户、添加域名、验证、移除)都要能通过 HTTP 以与 UI 相同的权限完成;若只能在控制台里做,说明平台泄漏到了 UI。
- 运行 pre-commit 清单中的证据命令,并把结果粘贴到输出中。
- 避免常见坑:租户头要设在请求上而不是响应上,用 NextResponse.next({ request: { headers: requestHeaders } });否则 Server Components 的 headers() 读不到。
- 避免转发入站租户头:curl -H "x-tenant-id: <other>" 会读到别的租户数据;在代理的每条路径上删除或覆盖 x-tenant-*,包括跳过解析的路径。
- 注意 starter kit 的 matcher '/((?!api|_next|[\w-]+\.\w+).*)' 会排除所有带扩展名的根文件,导致 robots.txt 和 sitemap.xml 跳过代理、每个租户都拿到平台的 /public 副本;要匹配它们并重写进租户段。
- 注意 Next.js 16 把 middleware.ts 改名为 proxy.ts(导出 proxy,Node.js 运行时,配置 runtime 会抛错);用 npx @next/codemod@canary middleware-to-proxy . 迁移;matcher 排除某路径也会跳过该路径上的 Server Function POST,所以租户检查也要放在数据层。
- Global Config(原 Edge Config)键名必须匹配 ^[\w-]+$,tenant_acme.com 会被拒绝;用无冲突编码或哈希,把点替换成下划线可能让不同主机名映射到同一个键;写入传播最多 10 秒,所以刚写完就读 Global Config 的“域名已连接”页面会显示旧状态,那里要读数据库;旧版 @vercel/edge-config SDK 读不到改名后连接的存储(它们创建 GLOBAL_CONFIG 而非 EDGE_CONFIG)。
- RLS 会被超级用户和 BYPASSRLS 角色绕过;表所有者除非启用 FORCE ROW LEVEL SECURITY 否则也绕过;以迁移角色连接的应用即使策略“开着”也能看到所有租户;要用单独角色连接,加 ALTER TABLE ... FORCE ROW LEVEL SECURITY,并用 SET ROLE app_user 测试。
- 在连接池上事务外执行 SET app.tenant_id = ... 会持续到下一个请求;要在事务内用 set_config('app.tenant_id', $1, true);在 PgBouncer 事务模式下这是唯一安全形式。
- Vercel 上通配符 *.acme.app 若不使用 Vercel nameservers 永远拿不到证书:DNS-01 需要 Vercel 写 _acme-challenge;先把 ns1.vercel-dns.com 和 ns2.vercel-dns.com 指过去并重新添加 MX 记录。
- Vercel 上 /.well-known 是保留路径,不能被重写或重定向;把所有路径重写进 /s/[slug] 的代理会破坏 HTTP-01,自定义域名证书永远签不出来;要先放行它。
- Cloudflare for SaaS:回退源必须是 SaaS zone 内的代理记录;自定义主机名等于 zone 名不受支持;当客户 zone 也在 Cloudflare 上时(O2O,标记为 cf-connecting-o2o: 1),_cf-custom-hostname 预验证不工作。
- untrusted dispatch namespace(默认)没有 request.cf 也没有 caches.default,租户代码读 request.cf.country 会抛错;trusted 模式恢复它们,但整个 namespace 内所有租户 Worker 共享一个缓存。
- KV 是最终一致的(最多 60 秒,负查询会被缓存):dispatch Worker 首次查询后新增的主机名会 404 一分钟;入驻期间未命中要回退到 D1。
- PSL 拒绝剩余注册期不足两年的域名;合并后 _psl.<suffix> TXT 仍保留;浏览器按自己的发布周期更新列表;列入 PSL 也会让 Domain=acme.app cookie 失效,包括你自己放在那里的跨子域 SSO。
- 一开始用路径方案、把自定义域名放在路线图上,意味着以后要做 URL 重写、cookie 变更和 DNS 迁移。
- 域名配额和费用因提供商和套餐而异;在定价前把当前官方限额及其访问日期放进套餐表。
适用边界 · 注意事项
- 租户不发布代码、只做内容/品牌差异且用 Vercel 单部署时,不需要 Cloudflare dispatch namespace 那套代码级隔离。
- 不要用 Cloudflare 代理挡在 Vercel 应用前面,会翻倍 TLS 和重定向层,导致重定向循环和证书签发失败。
- 若租户必须发布自己的代码,不要在 Vercel 单部署应用内做沙箱,应改用 Vercel 多项目或 Cloudflare。