You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js 13 App Router中如何获取原始请求体计算SHA-256签名?

在Next.js 13 App Router中验证Lemon Squeezy Webhook签名

要解决这个问题,核心是获取原始请求体字节流——因为Lemon Squeezy的签名是基于原始请求的原始内容计算的,而你之前用JSON.stringify(body)生成的内容,会和原始请求体存在格式差异(比如空格、键顺序、换行符),导致签名验证失败。

解决步骤

  1. 直接读取原始请求体的Buffer,避免解析后再序列化
  2. 用原始Buffer计算HMAC签名,和请求头的x-signature做对比
  3. 验证通过后再解析原始Buffer为JSON(用于后续业务逻辑)

完整代码示例

// app/api/webhooks/lemonsqueezy/route.ts
import { NextResponse } from 'next/server';
import crypto from 'crypto';

// 签名验证工具函数
const validateSignature = (rawBody: Buffer, requestSignature: string | null): boolean => {
  const secret = process.env.LMS_SECRET ?? '';
  if (!secret || !requestSignature) return false;

  // 基于原始请求体计算HMAC
  const hmac = crypto.createHmac('sha256', secret);
  const calculatedHash = hmac.update(rawBody).digest('hex');

  // 用timingSafeEqual避免时序攻击
  const digest = Buffer.from(calculatedHash, 'utf8');
  const signature = Buffer.from(requestSignature, 'utf8');
  return digest.length === signature.length && crypto.timingSafeEqual(digest, signature);
};

export async function POST(request: Request) {
  try {
    // 1. 读取原始请求体的Buffer
    const rawBody = await request.arrayBuffer();
    const rawBodyBuffer = Buffer.from(rawBody);

    // 2. 获取请求头中的签名
    const requestSignature = request.headers.get('x-signature');

    // 3. 验证签名
    const isValid = validateSignature(rawBodyBuffer, requestSignature);
    if (!isValid) {
      console.error('签名验证失败');
      return NextResponse.json({ error: '无效签名' }, { status: 403 });
    }

    // 4. 验证通过后,解析原始Buffer为JSON(用于业务逻辑)
    const body = JSON.parse(rawBodyBuffer.toString('utf8'));

    // 这里处理你的Webhook业务逻辑,比如更新订单状态、触发通知等
    console.log('Webhook数据:', body);

    return NextResponse.json({ success: true }, { status: 200 });
  } catch (error) {
    console.error('处理Webhook失败:', error);
    return NextResponse.json({ error: '服务器内部错误' }, { status: 500 });
  }
}

关键说明

  • 不能用request.json():因为该方法会读取并解析流,且ReadableStream只能被读取一次;同时解析后的JSON对象再序列化,会改变原始请求的格式细节,导致签名不匹配。
  • 必须用arrayBuffer()读取原始内容:转换成Buffer后直接用于HMAC计算,才能和Lemon Squeezy生成签名时的原始内容完全一致。
  • 始终使用crypto.timingSafeEqual对比签名:避免时序攻击,保证验证的安全性。

内容的提问来源于stack exchange,提问作者Christian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 04:46:26