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

Shopify Webhook HMAC验证失败,求排查代码实现问题

Shopify Webhook HMAC验证始终失败的排查与解决

问题描述

我正在开发一款Shopify应用,尝试通过下述函数验证app_subscriptions/update主题Webhook的HMAC签名,但无论用===还是timingSafeEqual对比,函数始终返回false,生成的Hash和请求头里的HMAC值始终不匹配。代码如下:

async function validateHmac(req: Request) {
  let shopifyApiSecret = config.shopifyAppSecret;

  let hmac: any = req.headers["x-shopify-hmac-sha256"];
  const message = JSON.stringify(req.body);
  const generatedHash = crypto
    .createHmac("sha256", shopifyApiSecret)
    .update(message)
    .digest("base64");
  console.log({ message, generatedHash, hmac });

  const signatureOk = crypto.timingSafeEqual(
    Buffer.from(generatedHash),
    Buffer.from(hmac)
  );
  return signatureOk;
}

核心问题分析及解决方案

  • 错误核心:使用解析后的JSON对象重新序列化计算HMAC
    Shopify计算HMAC签名时,用的是Webhook请求发送的原始HTTP请求体字节流,而不是你通过框架解析后得到的req.body再用JSON.stringify()序列化的结果。框架(比如Express、Next.js)在解析req.body时,会自动处理空格、键的排序、转义字符等,导致JSON.stringify(req.body)的结果和Shopify发送的原始请求体完全不一致,自然计算出的HMAC无法匹配。

  • 正确的处理方式

    1. 获取原始未修改的请求体:在解析req.body之前,先读取并保存原始的请求体文本。例如:
      • 在Express中,需要使用express.raw({ type: 'application/json' })中间件直接获取原始Buffer;
      • 在Next.js App Router中,用await req.text()获取原始文本,再手动解析为JSON:
        const rawBody = await req.text();
        const body = JSON.parse(rawBody);
        
    2. 用原始请求体计算HMAC:用获取到的原始请求体(字符串或Buffer)作为update()方法的参数,而不是JSON.stringify(req.body):
      const generatedHash = crypto
        .createHmac("sha256", shopifyApiSecret)
        .update(rawBody) // 这里用原始请求体
        .digest("base64");
      
  • 额外注意事项

    • 确保shopifyApiSecret确实是应用的Client Secret(不是API Key);
    • 使用timingSafeEqual时,要确保两个Buffer的长度一致,原始HMAC需用Buffer.from(hmac, 'base64')转换,避免编码问题;
    • 不要对原始请求体做任何修改(比如去除空格、格式化JSON),保持和Shopify发送的完全一致。

内容的提问来源于stack exchange,提问作者D.K. Goutham Kishore

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 12:07:23