繁体   English   中英

如何验证 Stripe 的 webhook 签名?

[英]How to verify Stripe's webhook signature?

感谢@user9014097 的广泛回答,我已经解决了这个问题。 本节特别描述了我的错误/疏忽:

消息的格式在确定 MAC 中起作用。 每个差异,例如换行、空白等都会改变签名并导致验证失败。 检查您是否可能稍微更改了消息或其格式。

对请求正文进行字符串化之后它就像一个魅力! 使用 cloudflare 工作人员,您可以像这样以纯文本形式获取原始正文: const payload = await event.request.text();

原帖:

我正在尝试手动验证 Stripe webhooks 的签名。 我不在 node.js 中工作,所以很遗憾,stripe-node 包不是我的选择。 我遵循了https://stripe.com/docs/webhooks/signatures#verify-manually上的“手动验证签名”步骤。 到目前为止,我已经制作了以下内容:

  • 正文:event.request.body(来自 cloudflare 工作人员的 fetch 事件)
  • 标头: event.request.headers.get('Stripe-Signature')
const hexStringToUint8Array = hexString => {
  const bytes = new Uint8Array(Math.ceil(hexString.length / 2));
  for (let i = 0; i < bytes.length; i++)
    bytes[i] = parseInt(hexString.substr(i * 2, 2), 16);
  return bytes;
};

export const verifySignature = async (body, header, tolerance = 300) => {
  header = header.split(',').reduce((accum, x) => { 
    const [k, v] = x.split('=');
    return { ...accum, [k]: v };
  }, {});
  
  const encoder = new TextEncoder();
  
  const key = await crypto.subtle.importKey(
    "raw",
    encoder.encode(STRIPE_WEBHOOK_SECRET),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["verify"]
  );

  const verified = await crypto.subtle.verify(
    "HMAC",
    key,
    hexStringToUint8Array(header.v1),
    encoder.encode(`${header.t}.${body}`)
  );

  const elapsed = Math.floor(Date.now() / 1000) - Number(header.t);
  return verified && !(tolerance && elapsed > tolerance)
}; 

但是验证函数总是返回false。 任何人都可以在这里发现问题吗?

谢谢你,杰科

编辑:下面是测试数据。 感谢@user9014097 的请求:

正文和标题应该用作verifySignature 的参数。

身体

{
  "id": "evt_1JAc3EFj0bmn6HyUJpVSB1hC",
  "object": "event",
  "api_version": "2020-08-27",
  "created": 1625669316,
  "data": {
    "object": {
      "id": "prod_Jkre4DaakpOaCt",
      "object": "product",
      "active": true,
      "attributes": [

      ],
      "created": 1624892313,
      "description": null,
      "images": [
        "https://files.stripe.com/links/MDB8YWNjdF8xSXR3Y3BGajBibW42SHlVfGZsX3Rlc3RfaHlGSVhUVHFmOHVLSzFhUUVGV0FWNlc300bY6GO8NE"
      ],
      "livemode": false,
      "metadata": {
        "brand": "DOM",
        "series": "1D",
        "key_codes_start": "1",
        "key_codes_end": "114"
      },
      "name": "DOM 1D serie 1-114",
      "package_dimensions": null,
      "shippable": null,
      "statement_descriptor": null,
      "type": "service",
      "unit_label": "sleutel",
      "updated": 1625669316,
      "url": null
    },
    "previous_attributes": {
      "description": "test",
      "updated": 1625665952
    }
  },
  "livemode": false,
  "pending_webhooks": 1,
  "request": {
    "id": "req_SxhB93mIUlcaKW",
    "idempotency_key": null
  },
  "type": "product.updated"
}

标题

t=1625700981,v1=08a60e9f42416808d1fbd3efb852695830af8f7e0da71d351dd5fbbf135d7974,v0=3ff951917ae810ac14236a0db7ce046011a1ce4d949f267650766b1c9bb1b3e2

verifySignature 函数中的 STRIPE_WEBHOOK_SECRET 变量用于导入/创建密钥。 然后用于验证有效负载/主体。 为了测试它,您可以替换下面秘密字符串的变量名称。

STRIPE_WEBHOOK_SECRET

whsec_bPYHe4WqVVG9jri3FwIHBLZgQSHTIS6j

虽然您不能在 Cloudflare Worker 中使用整个 Stripe 节点库,但您可以使用 webhook 签名片

在我的机器上,签名的实际验证是成功的!

但是,您的验证也会考虑时间戳。 如果验证时间与此时间戳的差异超过给定的容差值(默认为 300 秒),则验证失败。 正是这最后一个条件导致验证失败。

如果容差足够或者消息时间戳在容限内,则验证成功:

 (async () => { const hexStringToUint8Array = hexString => { const bytes = new Uint8Array(Math.ceil(hexString.length / 2)); for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hexString.substr(i * 2, 2), 16); return bytes; }; const verifySignature = async (body, header, tolerance = 300) => { header = header.split(',').reduce((accum, x) => { const [k, v] = x.split('='); return { ...accum, [k]: v }; }, {}); const encoder = new TextEncoder(); const key = await crypto.subtle.importKey( "raw", encoder.encode('whsec_bPYHe4WqVVG9jri3FwIHBLZgQSHTIS6j'), { name: "HMAC", hash: "SHA-256" }, false, ["verify"] ); const verified = await crypto.subtle.verify( "HMAC", key, hexStringToUint8Array(header.v1), encoder.encode(`${header.t}.${body}`) ); const elapsed = Math.floor(Date.now() / 1000) - Number(header.t); return verified && !(tolerance && elapsed > tolerance) }; var body = `{ "id": "evt_1JAc3EFj0bmn6HyUJpVSB1hC", "object": "event", "api_version": "2020-08-27", "created": 1625669316, "data": { "object": { "id": "prod_Jkre4DaakpOaCt", "object": "product", "active": true, "attributes": [ ], "created": 1624892313, "description": null, "images": [ "https://files.stripe.com/links/MDB8YWNjdF8xSXR3Y3BGajBibW42SHlVfGZsX3Rlc3RfaHlGSVhUVHFmOHVLSzFhUUVGV0FWNlc300bY6GO8NE" ], "livemode": false, "metadata": { "brand": "DOM", "series": "1D", "key_codes_start": "1", "key_codes_end": "114" }, "name": "DOM 1D serie 1-114", "package_dimensions": null, "shippable": null, "statement_descriptor": null, "type": "service", "unit_label": "sleutel", "updated": 1625669316, "url": null }, "previous_attributes": { "description": "test", "updated": 1625665952 } }, "livemode": false, "pending_webhooks": 1, "request": { "id": "req_SxhB93mIUlcaKW", "idempotency_key": null }, "type": "product.updated" }` var header = `t=1625700981,v1=08a60e9f42416808d1fbd3efb852695830af8f7e0da71d351dd5fbbf135d7974,v0=3ff951917ae810ac14236a0db7ce046011a1ce4d949f267650766b1c9bb1b3e2`; const elapsed = Math.floor(Date.now() / 1000) - Number(1625700981); console.log("Elapsed time in s:", elapsed) console.log("Verification without considering tolerance:", await verifySignature(body, header, null)); console.log("Verification with enough tolerance: ", await verifySignature(body, header, elapsed)); console.log("Verification with default tolerance: ", await verifySignature(body, header)); // default: tolerance = 300 })();

您环境中的验证失败可能有以下原因:

  • 容差太小(默认值 300s 同时 (!) 对于发布的 nessage 的时间戳来说太小了)。
  • 消息的格式在确定 MAC 中起作用。 每个差异,例如换行、空白等都会改变签名并导致验证失败。 检查您是否可能稍微更改了消息或其格式。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM