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

如何使用Node.js和Express验证Shopify Webhook?

Node.js + Express 验证 Shopify Webhook:正确获取请求体的方法

目前Stack Overflow上的相关教程都已过时,2023年暂无针对Node.js和Express验证Shopify Webhook的官方更新文档。核心问题在于:必须获取与Shopify生成HMAC时完全一致的原始请求体,才能让本地哈希和Shopify返回的HMAC匹配。

关键原理

Shopify生成HMAC签名时,是基于原始未解析的请求体字节流计算的。如果使用express.json()或bodyParser.json()这类中间件,它们会解析JSON并重新格式化(比如调整空格、排序键),导致请求体内容和Shopify计算时的版本不一致,最终哈希对比失败。

正确实现步骤

  1. 使用文本类型的请求体解析中间件:用bodyParser.text({ type: 'application/json' })(或Express 4.16+自带的express.text())获取原始的JSON字符串,避免解析过程中修改内容。
  2. 注意HMAC的编码格式:Shopify返回的X-Shopify-Hmac-Sha256是base64编码的,不是hex,计算本地哈希时要对应使用digest('base64')。
  3. 安全获取共享密钥:不要硬编码Shopify共享密钥,建议从环境变量或安全配置中心获取。

修正后的完整代码

import express from 'express'
import cors from 'cors'
import * as crypto from 'crypto'
import bodyParser from 'body-parser'

const app = express()
app.use(cors({ origin: true }))

// 关键:用text中间件获取原始JSON请求体,不解析不修改
app.post('/order/create', bodyParser.text({ type: 'application/json' }), async (req, res) => {
  try {
    const hmacHeader = req.header('X-Shopify-Hmac-Sha256')
    const topic = req.header('X-Shopify-Topic')
    const shopDomain = req.header('X-Shopify-Shop-Domain')
    
    // 从安全渠道获取Shopify共享密钥,示例为异步方法
    const sharedSecret = await shopifySharedSecret()
    if (!sharedSecret) {
      throw new Error('无法获取Shopify共享密钥')
    }

    // 用原始请求体生成HMAC,编码为base64(和Shopify一致)
    const generatedHmac = crypto
      .createHmac('sha256', sharedSecret)
      .update(req.body, 'utf8') // 指定编码确保字节流一致
      .digest('base64')

    // 对比签名(建议用crypto.timingSafeEqual防时序攻击)
    const isValid = crypto.timingSafeEqual(
      Buffer.from(generatedHmac),
      Buffer.from(hmacHeader)
    )

    if (!isValid) {
      throw new Error('HMAC验证失败')
    }

    // 验证通过,处理业务逻辑
    console.log(`收到来自 ${shopDomain} 的 ${topic} Webhook,内容:`, req.body)
    res.status(200).send({ processed: true })
  } catch (error) {
    console.error('Webhook处理失败:', error.message)
    res.status(403).send({ error: '验证失败' })
  }
})

// 示例:获取共享密钥的方法(替换为你的实际逻辑)
async function shopifySharedSecret() {
  // 比如从环境变量、数据库或配置中心获取
  return process.env.SHOPIFY_SHARED_SECRET
}

const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
  console.log(`服务器运行在端口 ${PORT}`)
})

额外注意事项

  • 时序攻击防护:用crypto.timingSafeEqual代替直接===对比,避免攻击者通过响应时间差异破解签名。
  • 请求头校验:除了HMAC,建议同时校验X-Shopify-Topic和X-Shopify-Shop-Domain,确保请求来源合法。
  • 错误处理:验证失败时返回403状态码,Shopify会在重试几次后停止发送该Webhook。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 07:45:36