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-SignatureHeader 中的值比较 - 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)