> 关键工具:Windsurf(操控代码)+ Supabase(数据库/鉴权)+ Stripe(支付)
> 技术栈阶:中高阶——"做成生意"的分水岭
> 完整演示:构思 → 创建项目 → 提示词编写 → AI 制作 → 项目检验 → 落地
> 预计耗时:跑通可收钱的版本约 4~6 小时(安全环节不能省)
这一篇怎么读
这是 9 个案例里 30% 含金量最高的一个——它同时碰"钱"和"多用户数据"。本篇完整演示从建项目到真实上线收订阅费,把"支付验签"和"多租户隔离"两道安全门焊死。
阶段一 · 构思:从一开始就把安全当及格线
- 痛点:独立开发者想做个能收订阅费的小 SaaS。
- 核心流程:注册登录 → 管理自己的客户名单 → 月度订阅付费。
- 两条不可逾越的红线(构思阶段就要立):Stripe 回调必须验签、数据库必须多租户隔离。这两条从第一行提示词就要在,而不是最后补。
- 成功标准:陌生人无法看到别人的数据,无法不付钱白嫖会员;能真实走通一笔订阅。
这个案例的特殊性
从这里开始,bug 不再是"体验不好",而是"资损"和"数据泄露"。做付费 SaaS,安全不是加分项,是及格线。
阶段二 · 创建项目:建工程 + 三个外部服务
这一步要把代码工程和三个外部服务(数据库、支付)都接好。
- 建工程:用 Windsurf 打开/新建一个 Next.js 项目(Windsurf 能深度理解多文件,适合这种有后端的工程)。
- 建 Supabase 项目:打开 supabase.com 新建 project,拿到
Project URL和anon key、service_role key。 - 建 Stripe 账号:打开 stripe.com 注册,先用测试模式(Test mode),拿到
Publishable key、Secret key;创建一个月度订阅 Product/Price。 - 配环境变量(
.env.local,并确认在.gitignore):
NEXT_PUBLIC_SUPABASE_URL=...
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
SUPABASE_SERVICE_ROLE_KEY=... # 仅服务端用,切勿暴露给前端
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_... # 配 webhook 时拿到
service_role key 与 webhook secret
SUPABASE_SERVICE_ROLE_KEY 能绕过所有权限,只能在服务端用,绝不能进前端代码。这两个密钥一旦泄露,等于把数据库和支付门户钥匙交出去。
阶段三 · 具体提示词编写:把安全红线写进第一行
做付费 SaaS,提示词重心从"功能"转向"安全边界"。两个"【关键】"要直接写进工单。
起手提示词(在 Windsurf 中)
做一个 Mini-CRM SaaS:Next.js + Supabase + Stripe。分模块实现,
每个模块先讲方案再写代码。
【模块1·鉴权与数据模型】
- Supabase Auth 邮箱注册/登录。
- 表 customers: id, owner_id(=auth用户id), name, email, note, created_at。
- 【关键】给 customers 开 RLS:每个用户只能增删改查 owner_id=auth.uid()
的行;INSERT 要加 with check。先把 RLS 策略写出来给我看,并给测试方法。
【模块2·客户看板】登录后管理自己的客户,增删改查,现代科技感 UI。
【模块3·订阅付费】
- Stripe Checkout 做月度订阅。
- 【关键】用 Stripe Webhook 接收支付结果更新订阅状态,必须做签名验证。
先做模块1,重点把 RLS 讲清楚、给我可复现的越权测试。阶段四 · AI 制作:看产出了什么(关键片段)
Windsurf 先给鉴权和 RLS 策略。RLS 策略关键片段:
-- customers 表的 RLS(关键片段)
alter table customers enable row level security;
create policy "read own" on customers for select
using ( owner_id = auth.uid() );
create policy "insert own" on customers for insert
with check ( owner_id = auth.uid() ); -- 这条最易被漏!
create policy "modify own" on customers for update using ( owner_id = auth.uid() );
create policy "delete own" on customers for delete using ( owner_id = auth.uid() );
看起来齐全,但绝不能只看它"说"配了,必须亲自扮演攻击者验证——见下一步。
阶段五 · 项目检验(上):以攻击者视角焊死两道门
5.1 RLS:验证"别人看不到、改不了我的数据"
RLS 越权测试提示词
给我可复现的 RLS 测试:
- 注册 A、B 两用户。A 插 2 条客户,B 插 1 条。
- 验证 A 查询只返回 A 的 2 条、B 只返回 1 条。
- 用 B 的身份拿 A 某条客户的 id 直接发 update/delete,应被拒(影响 0 行)。
- 用 B 的身份尝试 insert 一条 owner_id 写成 A 的记录,应被拒。
用 curl 或脚本可复现地跑出来。第一次测试就抓到问题:初版漏了 INSERT 的 with check,B 能插入一条 owner_id=A 的伪造记录。补上才真正堵死。
安全红线
"开了 RLS"≠"配对了 RLS"。select/insert/update/delete 每个动作都要单独覆盖,尤其 INSERT 的 with check 极易漏。必须用"扮演攻击者"的测试验证,而不是相信 AI 说它配了。
5.2 Stripe 验签:堵死"伪造付款"
到付费模块,最致命的 30% 来了。AI 初版 Webhook 直接信任请求体里的 "success",没验签——任何人发个伪造"成功"就能白嫖会员。
强制验签提示词
当前 Webhook 直接信任请求内容,是严重漏洞。必须改:
- 用 stripe.webhooks.constructEvent(rawBody, sig, STRIPE_WEBHOOK_SECRET)
验签,失败直接 400,不做任何处理。
- 注意:验签要 raw body,不能用解析过的 JSON——关掉该路由的 bodyParser。
- 只有验签通过且事件是 checkout.session.completed 才更新订阅状态。// webhook 验签(关键片段)
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET);
} catch (e) {
return res.status(400).send('invalid signature'); // 伪造请求到此为止
}
if (event.type === 'checkout.session.completed') { /* 开通订阅 */ }
调试时撞到经典坑:验签一直失败——因为 Next.js 默认把 body 解析成了 JSON,而验签需要原始字节。关掉该路由 bodyParser 才好。
阶段五 · 项目检验(下):用 Stripe CLI 真实联调 + 验收清单
支付不能靠"看起来对",用官方 CLI 跑真实事件:
本地联调提示词
给我用 stripe CLI 测 webhook 的步骤:
- stripe listen --forward-to localhost:3000/api/webhook(会给一个 whsec_)
- stripe trigger checkout.session.completed 触发,确认验签通过、订阅更新。
- 再伪造一个无效签名请求,确认返回 400 被拒。| 检验项 | 操作 | 期望 |
|---|---|---|
| 多租户 | B 各种方式读/改 A 的客户 | 全部被拒 |
| 伪造支付 | 向 webhook 发无效签名请求 | 400、不开通 |
| 验签正向 | Stripe CLI 触发真实事件 | 正确开通 |
| 幂等 | 同一支付事件重复投递 | 不重复开通、不重复计费 |
| 回调丢失兜底 | 模拟 webhook 未达 | 前端从成功页回查能补上状态 |
阶段六 · 落地:从测试模式切到真实收款
- 部署:把项目部署到 Vercel,在 Environment Variables 里配齐所有 Supabase / Stripe 密钥(线上单独一份)。
- 配线上 Webhook:在 Stripe Dashboard → Developers → Webhooks,新增端点指向
https://你的域名/api/webhook,订阅checkout.session.completed,拿到线上的whsec_配进 Vercel 环境变量。 - 切换到 Live 模式:在 Stripe 把测试 key 换成正式 key(
sk_live_...),完成账户的收款资料/合规信息后才能真正收款。 - 上线前最后一次真实小额自测:用真实卡走一笔最小额订阅,确认从付款到开通全链路通,再开放给用户。
落地阶段的两个必查
①线上 webhook secret 和测试的不是同一个,必须用线上那个;②确认 service_role key 只在服务端、没泄进前端 bundle。这两点错了,要么支付全失败,要么数据库门户大开。
30% 复盘:人类到底守住了什么
AI 能快速搭出登录、看板、Checkout 跳转(70%,UI 还很漂亮)。让它"敢真收钱、敢真存客户数据"的 30%,全是安全攻坚:
| 30% 工程点 | 人类做了什么 | 不做会怎样 |
|---|---|---|
| 多租户隔离 | RLS 覆盖增删改查,补 INSERT 的 with check | 用户能看/改别人的客户数据 |
| 支付验签 | constructEvent 验签 + raw body | 任何人伪造"成功"白嫖,直接资损 |
| 真实联调 | Stripe CLI 正反两路测试 | "看起来对",上线即被薅 |
| 幂等与兜底 | 重复回调不重复开通、回调丢失前端补查 | 重复计费或付了钱没开通 |
核心教训:做玩具和做生意的分水岭,是你是否以"攻击者视角"对待了钱和数据。 AI 默认只铺"一切正常"的路,那条"有人故意作恶"的路永远要你亲自堵死。
可复用心法与提示词模板
带得走的三条
- 安全红线写进第一行提示词:验签、隔离从一开始就标"关键"。
- 不信 AI 说"配好了",用攻击者测试验证:RLS 和验签必须扮坏人打一遍。
- 支付/权限必须真实联调:用 Stripe CLI 跑正反两路(合法通过、伪造被拒)。
通用"付费 SaaS 安全"提示词骨架:
做一个 <SaaS>,技术栈 <...>。从一开始遵守两条安全红线:
1. 多租户隔离:每张用户表开 RLS,覆盖 select/insert/update/delete,
INSERT 必须 with check;给我可复现的"扮演他人"越权测试。
2. 支付安全:支付回调用官方 SDK 验签,失败即拒;用原始 body;开通逻辑幂等;
给我用官方 CLI 跑正反两路测试的步骤。
密钥走环境变量,service 级密钥仅服务端,绝不进前端。