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

如何在Node.js中实现微服务间带签名的Webhook请求收发

Webhook 签名可选方案

常用的方案分为两类,可根据场景选择:

  • 对称加密HMAC方案:两端共享同一个密钥,发送方用密钥生成签名,接收方用相同密钥生成签名比对。实现简单、性能高,适合内部微服务互信场景,缺点是密钥泄露后会完全失效。
  • 非对称加密RSA/ECDSA方案:发送方持有私钥签名,接收方持有公钥验签,不需要共享敏感密钥,安全性更高,适合跨机构/公开webhook场景,性能略低于HMAC。

HMAC-SHA256 实现示例(推荐内部微服务使用)

Service X(发送方)签名逻辑

  1. 两端提前约定共享密钥,存储在环境变量中,禁止硬编码
  2. 拼接时间戳、请求方法、请求路径、原始请求体为待签名字符串
  3. 用密钥生成HMAC-SHA256签名,和时间戳一起放入请求头发送
    代码示例:
const crypto = require('crypto');
const axios = require('axios');

const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
const SERVICE_Y_ENDPOINT = 'https://service-y.com/order/123456';

async function sendOrderWebhook(payload) {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const rawPayload = JSON.stringify(payload);
  // 待签名字符串格式要和接收端完全一致
  const signContent = `${timestamp}\nPOST\n/order/123456\n${rawPayload}`;

  const signature = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(signContent)
    .digest('hex');

  await axios.post(SERVICE_Y_ENDPOINT, payload, {
    headers: {
      'Content-Type': 'application/json',
      'X-Webhook-Timestamp': timestamp,
      'X-Webhook-Signature': `sha256=${signature}`
    }
  });
}

// 调用示例
sendOrderWebhook({ orderId: '123456', status: 'paid', amount: 9900 });

Service Y(接收方)验签逻辑

  1. 先获取请求原始body,不能用直接解析后的body,避免内容被修改导致签名不匹配
  2. 校验时间戳和当前时间差不超过5分钟,防止重放攻击
  3. 用相同逻辑生成签名,和请求头的签名做安全比对
    代码示例(基于Express):
const crypto = require('crypto');
const express = require('express');
const app = express();

const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
const MAX_TIME_DIFF = 5 * 60; // 允许5分钟内的请求

// 保留原始请求body
app.use(express.json({
  verify: (req, _, buf) => req.rawBody = buf.toString()
}));

app.post('/order/:orderId', (req, res) => {
  const timestamp = req.headers['x-webhook-timestamp'];
  const signHeader = req.headers['x-webhook-signature'];

  // 缺少必要参数直接拒绝
  if (!timestamp || !signHeader) return res.status(401).send('Missing signature headers');
  // 防重放校验
  if (Math.abs(Date.now()/1000 - Number(timestamp)) > MAX_TIME_DIFF) return res.status(401).send('Request expired');

  const [alg, signature] = signHeader.split('=');
  if (alg !== 'sha256' || !signature) return res.status(401).send('Invalid signature format');

  // 生成预期签名
  const signContent = `${timestamp}\nPOST\n${req.path}\n${req.rawBody}`;
  const expectedSign = crypto.createHmac('sha256', WEBHOOK_SECRET).update(signContent).digest('hex');

  // 安全比对,防止时序攻击
  const signBuf = Buffer.from(signature, 'hex');
  const expectedBuf = Buffer.from(expectedSign, 'hex');
  if (signBuf.length !== expectedBuf.length || !crypto.timingSafeEqual(signBuf, expectedBuf)) {
    return res.status(401).send('Invalid signature');
  }

  // 校验通过,处理业务逻辑
  res.status(200).send('OK');
});

app.listen(3000, () => console.log('Service Y running on port 3000'));

RSA-SHA256 非对称签名实现示例

Service X(发送方)签名代码

需提前生成RSA密钥对,私钥仅保存在Service X侧:

const crypto = require('crypto');
const axios = require('axios');
const fs = require('fs');

// 读取私钥,可从密钥管理服务获取
const PRIVATE_KEY = fs.readFileSync('./x_private.pem', 'utf8');
const SERVICE_Y_ENDPOINT = 'https://service-y.com/order/123456';

async function sendOrderWebhook(payload) {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const rawPayload = JSON.stringify(payload);
  const signContent = `${timestamp}\nPOST\n/order/123456\n${rawPayload}`;

  const signature = crypto
    .sign('sha256', Buffer.from(signContent), PRIVATE_KEY)
    .toString('base64');

  await axios.post(SERVICE_Y_ENDPOINT, payload, {
    headers: {
      'Content-Type': 'application/json',
      'X-Webhook-Timestamp': timestamp,
      'X-Webhook-Signature': `rsa-sha256=${signature}`
    }
  });
}

Service Y(接收方)验签代码

仅需持有Service X的公钥即可完成验签:

const crypto = require('crypto');
const express = require('express');
const fs = require('fs');
const app = express();

const PUBLIC_KEY = fs.readFileSync('./x_public.pem', 'utf8');
const MAX_TIME_DIFF = 5 * 60;

app.use(express.json({
  verify: (req, _, buf) => req.rawBody = buf.toString()
}));

app.post('/order/:orderId', (req, res) => {
  const timestamp = req.headers['x-webhook-timestamp'];
  const signHeader = req.headers['x-webhook-signature'];

  if (!timestamp || !signHeader) return res.status(401).send('Missing signature headers');
  if (Math.abs(Date.now()/1000 - Number(timestamp)) > MAX_TIME_DIFF) return res.status(401).send('Request expired');

  const [alg, signature] = signHeader.split('=');
  if (alg !== 'rsa-sha256' || !signature) return res.status(401).send('Invalid signature format');

  const signContent = `${timestamp}\nPOST\n${req.path}\n${req.rawBody}`;
  const isVerifyPass = crypto.verify(
    'sha256',
    Buffer.from(signContent),
    PUBLIC_KEY,
    Buffer.from(signature, 'base64')
  );

  if (!isVerifyPass) return res.status(401).send('Invalid signature');

  res.status(200).send('OK');
});

app.listen(3000);

注意事项
  • 所有密钥必须存储在环境变量、密钥管理服务中,禁止硬编码到代码仓库
  • 签名比对必须使用crypto.timingSafeEqual(HMAC场景),禁止使用普通字符串全等操作,避免时序攻击
  • 必须添加时间戳校验,避免攻击者捕获历史请求重放攻击
  • 待签名内容需包含请求核心信息(方法、路径、请求体、时间戳),避免攻击者篡改部分内容绕过校验

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 20:45:03