在NextJS中如何同时验证LemonSqueezy Webhook签名并使用请求体?
解决NextJS中LemonSqueezy Webhook签名验证与请求体复用问题
问题根源在于请求体是可读流,一旦被读取(比如验证签名时调用await req.text()),流就会被消耗,后续无法再通过req.json()读取。克隆请求无效是因为NextJS的请求对象在读取后,克隆也无法恢复已消耗的流。
直接通过以下步骤解决:
- 一次性读取原始请求体文本并保存,同时用于签名验证和数据解析
- 避免多次读取请求流
Pages Router 完整代码示例
import type { NextApiRequest, NextApiResponse } from 'next'; import crypto from 'crypto'; const WEBHOOK_SECRET = process.env.LEMON_SQUEEZY_WEBHOOK_SECRET!; export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method !== 'POST') { return res.status(405).end(); } // 1. 读取原始请求体文本,仅读取一次 const rawBody = await new Promise<string>((resolve, reject) => { let body = ''; req.on('data', chunk => body += chunk.toString()); req.on('end', () => resolve(body)); req.on('error', reject); }); // 2. 验证签名 const signature = req.headers['x-lemon-squeezy-signature'] as string; const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET); const digest = Buffer.from(hmac.update(rawBody).digest('hex'), 'utf8'); const signatureBuffer = Buffer.from(signature, 'utf8'); if (!crypto.timingSafeEqual(digest, signatureBuffer)) { return res.status(401).json({ message: 'Invalid signature' }); } // 3. 解析请求体为JSON并使用 try { const payload = JSON.parse(rawBody); // 这里编写业务逻辑,比如更新数据库、发送通知等 console.log('Webhook payload:', payload); return res.status(200).json({ message: 'Success' }); } catch (err) { return res.status(400).json({ message: 'Invalid JSON payload' }); } } // 关键配置:禁用NextJS默认请求体解析,避免提前消耗流 export const config = { api: { bodyParser: false, }, };
App Router(NextJS 13+)代码示例
import { NextRequest, NextResponse } from 'next/server'; import crypto from 'crypto'; const WEBHOOK_SECRET = process.env.LEMON_SQUEEZY_WEBHOOK_SECRET!; export async function POST(req: NextRequest) { // 读取原始请求体文本 const rawBody = await req.text(); // 验证签名 const signature = req.headers.get('x-lemon-squeezy-signature')!; const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET); const digest = Buffer.from(hmac.update(rawBody).digest('hex'), 'utf8'); const signatureBuffer = Buffer.from(signature, 'utf8'); if (!crypto.timingSafeEqual(digest, signatureBuffer)) { return NextResponse.json({ message: 'Invalid signature' }, { status: 401 }); } // 解析并使用payload const payload = JSON.parse(rawBody); // 编写业务逻辑 console.log('Webhook payload:', payload); return NextResponse.json({ message: 'Success' }); }
关键说明
- 禁用默认bodyParser(Pages Router):NextJS默认会自动解析请求体,导致无法获取原始未解析文本用于签名验证,必须手动禁用。
- 复用原始文本:通过一次读取获取完整原始请求体,既用于签名验证,又解析成JSON使用,避免流被多次消耗。
- timingSafeEqual安全验证:避免时序攻击,是签名验证的安全最佳实践。
内容的提问来源于stack exchange,提问作者Rik Nieu
相关产品推荐
相关产品推荐

