本地开发中大多数 Shopify Webhook 错误归结于一行:HMAC 是 base64 编码的,不是 hex。如果你调用 .digest('hex') 并与 X-Shopify-Hmac-Sha256 比较,检查永远不会通过——即使有正确的共享密钥。我们见过资深工程师为这一个字符耗掉一个下午。
base64 陷阱
Stripe 用 hex。GitHub 用 hex(带 sha256= 前缀)。Shopify 用 base64。三个提供商,三种编码。你的验证器第一版几乎肯定会从另一个项目复制片段并悄悄失败。
验证应当是这样:
const hmac = crypto
.createHmac('sha256', SHOPIFY_API_SECRET)
.update(rawBody) // raw body, not JSON-parsed
.digest('base64'); // base64, not hex
const valid = crypto.timingSafeEqual(
Buffer.from(hmac),
Buffer.from(req.headers['x-shopify-hmac-sha256']),
);
编码之外还有两点重要。body 必须是原始的、未解析的字节。比较必须是时间安全的——字符串相等会一次泄漏一个字节的信息。
为什么隧道让这更容易
你也可以用 Shopify 的 shopify app dev 命令,它会启动一个内部隧道。能用。但它也把你的开发服务器包进自己的进程,以特定方式吞掉日志,且不给你重放按钮。对于超出 "hello world" 的应用开发,一个稳定的公开 URL 加上带请求捕获的 localhost 隧道,省下的时间比 CLI 在初始设置上省下的更多。
npx portpreview 3000
你把 HTTPS URL 粘贴到 Partners 仪表盘中应用的 webhook 配置里,从测试店触发一个事件,请求就带着所有 header 完好地落到你的本地处理器。
测试店:Shopify 文档略过的部分
接线之前要知道两件事:
- 开发店会触发所有 webhook 事件。订单创建、履约、库存——全都。你不需要伪造任何东西;只要把应用装到开发店上点点看。
- webhook 密钥按应用和按投递通道而不同。如果你还订阅了 EventBridge 或 Pub/Sub,HMAC 行为不同。这里说的是纯 HTTPS webhook 投递。
分步设置
- 在你使用的端口上本地启动应用(3000 常见)。
- 运行
npx portpreview 3000并复制它打印的 HTTPS URL。 - 在 Shopify Partners 仪表盘打开你的应用,进入 Configuration → Webhooks。
- 把 webhook 端点设为
https://your-tunnel.portpreview.dev/api/webhooks/shopify(或你应用使用的路径)。 - 把应用装到开发店,然后触发事件——创建草稿订单、履约一个商品、改库存。
- 看请求落地。检查 header 和 body。每次修复处理器后重放捕获的请求。
我们反复看到的错误
验证前就解析了 body
如果你把 app.use(express.json()) 放在 webhook 路由之前,等你尝试验证时原始字节已经没了。只在 webhook 路径上挂载原始 body 解析器,或者在任何 JSON 解析之前手动从请求流里拉 body。
搞混了密钥
Partners 应用密钥不等于 Storefront API 令牌。webhook HMAC 用的是应用密钥。如果你盯着 env 文件里的 shp_xxx,你拿错了。
看起来完全一样的测试事件
Shopify 的 "Send test notification" 按钮投递一个带占位 body 的合成事件。那个测试事件上的签名是真的,但 payload 是固定的。要做真实的 payload 形状测试,从开发店本身触发事件。
对应用开发来说重放不可妥协
Shopify 对失败的 webhook 以指数退避重试最多 48 小时。很慷慨,但当你迭代时你不想等下一次重试。在 你隧道的请求历史里捕获首次投递,并按需对你的本地处理器重放它。
本地何时不再够用
店铺注册流程、GDPR webhook 和订阅计费变更更容易对着已部署环境验证,因为它们与 Shopify 自己的账户状态交互。其余的一切——payload 解析、签名验证、业务逻辑——本地更快。
关于所有提供商底层的签名数学,阅读我们的webhook 签名验证指南。如果你想要一个内置重放的隧道,加入 PortPreview 等候名单。