要在本地主机上测试 Lemon Squeezy webhooks,启动您的应用程序,通过公共 HTTPS 隧道公开其 webhook 路由,在设置 → Webhooks 中创建一个测试模式 webhook,并验证原始请求正文。 X-Signature 标题。 使用测试结账来处理真实订单数据,并使用 Lemon Squeezy 的订阅模拟控制来处理难以等待的生命周期事件。
正确的本地设置是什么样的
Lemon Squeezy 发送一个 HTTP POST 来自其基础设施,因此无法到达 localhost 在你的电脑上。该隧道终止公共 TLS,并将未更改的方法、正文和头部转发到本地路由,例如 http://localhost:3000/api/webhooks/lemonsqueezy.
完整的测试有四个部分:Lemon Squeezy 测试模式、一个公共回调 URL、在仪表板和本地环境中相同的签名密钥,以及一个在更改订阅或许可状态之前进行验证的处理程序。这比发布复制的 JSON 更有用。 curl,因为实际交付同时测试了路由和供应商生成的签名。
创建 webhook 并发送测试事件
- 在端口3000上运行你的应用程序,并验证该路由是否被接受
POST. - 用……开始隧道
npx portpreview 3000。 - 将 Lemon Squeezy 仪表板切换到测试模式。
- 打开设置 → Webhooks 并添加
https://your-subdomain.portpreview.dev/api/webhooks/lemonsqueezy. - 生成一个强随机签名密钥,并将其保存为
LEMONSQUEEZY_WEBHOOK_SECRET,并仅选择您的应用程序使用的事件。 - 创建一次测试购买或使用可用的测试模式订阅模拟操作,然后检查交付日志和您的本地响应。
官方 Lemon Squeezy 网络钩子文档 让您可以从设置页面检查最近的有效负载并重新发送已记录的 Webhook。重新发送对于回归测试非常有用,但您的应用程序必须将其视为重复而不是第二个业务事件。
验证 X-Signature 解析 JSON 之前
Lemon Squeezy 使用签名密钥对请求负载计算 HMAC-SHA256 摘要,并发送十六进制摘要 X-Signature计算整个原始正文字节的摘要,并使用恒定时间函数比较等长缓冲区。主要 签署请求文档 包括 Node、PHP、Python 和 Ruby 的示例。
// app/api/webhooks/lemonsqueezy/route.ts
import crypto from 'node:crypto';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const secret = process.env.LEMONSQUEEZY_WEBHOOK_SECRET;
if (!secret) return new NextResponse('server misconfigured', { status: 500 });
const rawBody = await request.text();
const suppliedHex = request.headers.get('x-signature') ?? '';
if (!/^[0-9a-f]{64}$/i.test(suppliedHex)) {
return new NextResponse('invalid signature', { status: 401 });
}
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest();
const supplied = Buffer.from(suppliedHex, 'hex');
if (supplied.length !== expected.length ||
!crypto.timingSafeEqual(supplied, expected)) {
return new NextResponse('invalid signature', { status: 401 });
}
const event = JSON.parse(rawBody);
await acceptEvent(event);
return NextResponse.json({ received: true });
}
不要打电话 request.json() 首先。重新序列化生成的对象可能会改变看似无关紧要的字节并破坏 HMAC。长度检查也是必要的,因为 Node 的 timingSafeEqual 当缓冲区长度不同时抛出异常。Lemon Squeezy 的官方 Next.js 网络钩子教程 遵循相同的原始主体序列。
在更新数据库之前先了解有效载荷
该主体是一个 JSON:API 资源对象。 meta.event_name 识别事件, meta.custom_data 携带通过结账传递的数据,并且 data 包含订单、订阅或许可密钥资源。Lemon Squeezy 也会发送 X-Event-Name; 将已签名的正文视为权威输入,并使用头信息进行路由诊断。
订单与客户身份
为了 order_created将购买映射到使用服务器生成的标识符的内部用户的结账自定义数据。不要在未经验证所有权的情况下信任客户端提供的用户ID。保存Lemon Squeezy的资源ID和变体ID,以便后来可以对订阅或退款事件进行对账。
订阅是一个状态机
把手 subscription_created, subscription_updated, subscription_cancelled, subscription_resumed,和 subscription_expired 作为不同的过渡。取消可能意味着访问在账单周期结束之前仍然有效;到期是订阅已结束的更强信号。建模日期和状态,而不是切换一个布尔值。
支付失败与恢复
测试失败并恢复续订流程,其中测试模式支持它们。失败的付款不应立即撤销访问权限;应根据您的产品政策应用宽限期,并让后续的付款或订阅事件将账户调整到最新的提供商状态。
重试需要幂等处理
根据 Lemon Squeezy 的 Webhook 请求参考,一个 200 响应标记捕获为成功。其他状态会重试最多三次,指数延迟大约为 5、25 和 125 秒。仪表板重新发送会导致同一逻辑事件可能多次到达。
从稳定的签名有效负载字段构建幂等性密钥。一种实用方法是为 webhook 的资源 ID、事件名称和事件时间戳创建唯一的数据库行,或者在没有专用事件 ID 的情况下对原始签名主体进行哈希。在一次事务中插入该收据并更新应用程序状态。返回 200 用于之前已完成的重复项。
BEGIN;
INSERT INTO webhook_receipts (fingerprint, received_at)
VALUES ($1, now())
ON CONFLICT DO NOTHING;
-- Continue only if one row was inserted.
UPDATE subscriptions SET provider_status = $2 WHERE provider_id = $3;
COMMIT;
不要将电子邮件发送或慢速第三方 API 调用作为确认路径的一部分。在同一事务中存储作业,响应,然后让工作线程处理副作用。更广泛的 Webhook 重试和幂等性指南 涵盖这个模式。
解决 Lemon Squeezy webhook 本地主机测试问题
没有请求到达应用程序
检查隧道是否仍在运行,回调是否包含路由后缀,以及你的框架在该精确路径上是否有 POST 处理程序。自己调用公共端点。隧道 502 指向本地端口;框架 404 指向路由位置。
每个签名都无效
确认测试模式和实时模式没有使用不同的 webhook 记录或密钥。将正文读取一次作为文本或字节,不要修剪正文或密钥,并确保全局 JSON 中间件不会先运行。检查是否 X-Signature 已到达,但切勿打印签名密钥。
投递正在重试
Lemon Squeezy 期望 200,而不是重定向到登录页面。在保留签名验证的同时,将 webhook 路径从 CSRF 和用户会话中间件中排除。捕获数据库错误,保持工作简短,并确保在每个处理的事件分支上确实返回成功响应。
订阅模拟不可用
模拟控制需要合适的测试模式订阅数据。某些付款模拟需要续订历史记录。首先创建所需的测试订阅或测试购买,然后使用 Lemon Squeezy 中描述的订阅操作菜单。 Webhook 开发者指南.
安全与上线清单
- 生成一个随机签名密钥,将其存储在环境配置中,并在泄露时进行轮换。
- 在进行 JSON 解析、数据库写入、许可证生成或访问更改之前,验证原始主体上的 HMAC。
- 使用计时安全的比较,并拒绝缺失或格式错误的签名。
- 保持测试模式和实时 webhook 的密钥及记录分开。
- 只允许 POST,强制执行合理的请求体大小限制,对无效请求进行速率限制,并从日志中删除客户数据。
- 在用生产 URL 替换隧道之前,测试重复、生命周期事件的乱序、数据库停机和超时恢复。
一次通过的正常流程结账只证明一个事件已经到达。一个可投入生产的集成证明了签名拒绝、重复安全、生命周期收敛以及从非200响应中恢复。有关潜在的加密陷阱,请参见 签名验证指南.
