帮助中心Webhook文档 › 签名验证
WEBHOOK

Webhook 签名验证

使用 HMAC-SHA256 验证每条 Webhook 请求的真实性,防止伪造请求攻击

为什么需要签名验证

翠鸟 Webhook 端点是公开可访问的 HTTP 接口,任何人都可以向其发送 POST 请求。如果不验证请求来源,攻击者可以伪造 Webhook 事件,诱使您的系统执行未授权操作(如创建虚假线索)。

通过签名验证,您可以确认每条请求确实来自翠鸟服务器,而非伪造请求。

安全要求:生产环境的 Webhook 处理器必须实现签名验证。跳过验证会使您的端点暴露于重放攻击和伪造请求风险。

签名机制说明

翠鸟使用 HMAC-SHA256 算法对每条 Webhook 请求进行签名,签名值通过请求头 X-Cuiniao-Signature 传递。

签名计算步骤:

  • 1. 获取请求头 X-Cuiniao-Timestamp 中的 Unix 时间戳
  • 2. 拼接待签名字符串:{timestamp}.{raw_request_body}
  • 3. 使用 Signing Secret 对上述字符串计算 HMAC-SHA256
  • 4. 将计算结果与 X-Cuiniao-Signature Header 中的值比较
  • 5. 同时检查时间戳是否在当前时间 ±5 分钟内,防止重放攻击
// 签名 Header 格式 X-Cuiniao-Signature: sha256=a1b2c3d4e5f6... X-Cuiniao-Timestamp: 1710492753

各语言实现示例

Java

JAVA
import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; public class WebhookVerifier { private final String signingSecret; public WebhookVerifier(String signingSecret) { this.signingSecret = signingSecret; } public boolean verify(String rawBody, String signature, String timestamp) { // 1. 检查时间戳有效期(±5分钟) long ts = Long.parseLong(timestamp); long now = System.currentTimeMillis() / 1000; if (Math.abs(now - ts) > 300) { return false; // 时间戳过期,拒绝请求 } // 2. 计算期望签名 String payload = timestamp + "." + rawBody; String expected = "sha256=" + hmacSha256(payload, signingSecret); // 3. 时间安全的字符串比较(防止时序攻击) return MessageDigest.isEqual( expected.getBytes(StandardCharsets.UTF_8), signature.getBytes(StandardCharsets.UTF_8) ); } private String hmacSha256(String data, String key) throws Exception { Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8)); return bytesToHex(hash); } }

Python

PYTHON
import hmac import hashlib import time def verify_webhook(raw_body: bytes, signature: str, timestamp: str, secret: str) -> bool: # 检查时间戳(±5分钟) ts = int(timestamp) if abs(time.time() - ts) > 300: return False # 计算期望签名 payload = f"{timestamp}.{raw_body.decode('utf-8')}" expected = "sha256=" + hmac.new( secret.encode("utf-8"), payload.encode("utf-8"), hashlib.sha256 ).hexdigest() # 时间安全比较 return hmac.compare_digest(expected, signature)

Node.js

JAVASCRIPT
const crypto = require('crypto'); function verifyWebhook(rawBody, signature, timestamp, secret) { // 检查时间戳 const ts = parseInt(timestamp, 10); if (Math.abs(Date.now() / 1000 - ts) > 300) return false; // 计算签名 const payload = `.`; const expected = 'sha256=' + crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); // 时间安全比较 return crypto.timingSafeEqual( Buffer.from(expected), Buffer.from(signature) ); }
务必使用时间安全的字符串比较函数(Java 的 MessageDigest.isEqual、Python 的 hmac.compare_digest、Node.js 的 crypto.timingSafeEqual),防止时序攻击通过响应时间差猜测签名。

使用 SDK 内置验证器

如果您使用翠鸟官方 SDK,无需手动实现签名验证,直接使用内置的 WebhookVerifier 类:

// Java SDK WebhookVerifier verifier = new WebhookVerifier(System.getenv("CUINIAO_WEBHOOK_SECRET")); boolean valid = verifier.verify(rawBody, signatureHeader, timestampHeader); # Python SDK from cuiniao.webhook import WebhookVerifier verifier = WebhookVerifier(os.environ["CUINIAO_WEBHOOK_SECRET"]) valid = verifier.verify(raw_body, signature, timestamp)