
在一次为创业团队上线订阅系统时,我意外发现一条未授权的付款记录——原来是 webhook 验签环节的细节疏漏导致的。自此,围绕 Supabase 与 Stripe 的安全拼图,我把每一块都拼得紧紧的。
Supabase 通过 JWT 把用户会话封装进 access_token,而 Stripe 则依赖 client_secret 完成支付确认。两者共存时,最安全的做法是:
access_token,绝不把 Stripe 的 publishableKey 之外的任何密钥写入 HTML。STRIPE_SECRET_KEY,并在每次调用 Stripe API 前动态注入。auth.jwt() 验证用户角色,将支付权限限制在 “subscriber” 或 “admin”。Stripe 会在请求头中放置 Stripe-Signature,我们必须在 Edge Function 的原始 body 上做 HMAC‑SHA256 比对,切记使用时间安全比较防止侧信道攻击。
import { createClient } from '@supabase/supabase-js';
import Stripe from 'stripe';
import crypto from 'crypto';
const supabase = createClient(Deno.env.get('SUPABASE_URL'), Deno.env.get('SUPABASE_ANON_KEY'));
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY'), { apiVersion: '2023-10-16' });
export const handler = async (req) => {
const sig = req.headers.get('stripe-signature');
const payload = await req.text(); // 保持原始字节流
const secret = Deno.env.get('STRIPE_WEBHOOK_SECRET');
const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex');
const provided = sig.split(',')[1].split('=')[1];
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(provided))) {
return new Response('Invalid signature', { status: 400 });
}
const event = stripe.webhooks.constructEvent(payload, sig, secret);
// 业务逻辑…
return new Response('OK', { status: 200 });
};
Supabase 的行级安全(RLS)让我们可以把支付记录锁在特定用户下。示例策略:
-- 只允许拥有 matching stripe_customer_id 的用户读取 payments 表
create policy "customer_can_read_own_payments"
on payments
for select
using (auth.jwt() ->> 'stripe_customer_id' = stripe_customer_id);
上述规则依赖于在登录时把 Stripe 客户 ID 注入 JWT 声明中,避免在前端再次查询。
一旦 webhook 验签失败或支付状态异常,系统应该立刻触发两条链路:
functions_log 写入详细错误栈,配合 Vercel 或 Railway 的日志聚合平台实现实时告警。“Never trust data that comes from the client; always verify on the server.” — Stripe Security Guidelines
参与讨论
暂无评论,快来发表你的观点吧!