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

WhatsApp Cloud API:Webhook签名验证时而成功时而失败

Facebook Webhook签名验证时而成功时而失败的原因及修复

核心问题

你的验证逻辑存在致命问题:你用JSON.stringify(requestContext?.body)生成计算哈希的内容,但Facebook的签名是基于原始请求体的字节流计算的,不是框架解析后再序列化的JSON字符串。

当web框架把请求体解析成JSON对象后,再用JSON.stringify转成字符串,会出现多种导致哈希不匹配的情况:

  • 原始请求的空格、换行符被修改
  • JSON对象的键顺序可能和原始请求不一致
  • 字符串的引号转义方式可能不同

这些差异会让你计算出的哈希和Facebook发送的签名不一致,进而导致验证时而成功时而失败(取决于序列化后的字符串是否碰巧和原始请求一致)。

修复方案

直接使用原始请求体的原始数据(未经过框架解析的字节或字符串)计算哈希,而非解析后的JSON对象再序列化。

修正后的代码示例

假设你的框架允许获取原始请求体(比如Express中配置express.raw()中间件,NestJS中用@RawBody()装饰器):

static createSha256Hash(data: Buffer | string, key: string): string {
    if (!data || (typeof data === 'string' && data.length === 0)) {
        throw new ValidationException();
    }

    const sha256Hasher = crypto.createHmac('sha256', key);
    return sha256Hasher.update(data).digest('hex');
}
public validateWebhookSignature(requestContext: RequestContextModel): void {
    const signature = requestContext?.headers?.['X-Hub-Signature-256']?.replace('sha256=', '');
    // 直接使用原始请求体,而非解析后的body再序列化
    const rawBody = requestContext.rawBody; // 假设requestContext包含原始请求体的Buffer或字符串

    if (!signature || !rawBody) {
        throw new ValidationException();
    }

    const hash = CryptoUtil.createSha256Hash(rawBody, this._appSecret);
    if (signature !== hash) {
        throw new UnauthorizedException();
    }
}

额外注意事项

  • 确保web框架没有自动修改原始请求体,比如不要启用压缩、自动转码等会改变原始字节的配置。
  • 建议用crypto.timingSafeEqual比较哈希值,避免时序攻击(虽非当前直接问题,但属于最佳实践):
    const bufferSignature = Buffer.from(signature, 'hex');
    const bufferHash = Buffer.from(hash, 'hex');
    if (bufferSignature.length !== bufferHash.length || !crypto.timingSafeEqual(bufferSignature, bufferHash)) {
        throw new UnauthorizedException();
    }
    

内容的提问来源于stack exchange,提问作者Maor agai

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 15:00:15