所有文章
移动消息事件经过 Meta 式 Webhook 验证和安全隧道,传送到 localhost 应用。
WhatsApp Cloud APIMeta webhookslocalhostwebhook security

在 localhost 测试 WhatsApp Cloud API Webhook

要在本地主机上测试 WhatsApp Cloud API Webhook,请通过 HTTPS 公开您的本地端点,实施 Meta 的 GET 验证质询,然后根据原始正文验证每个 POST 请求。X-Hub-Signature-256在您的 Meta 应用程序中注册隧道 URL,订阅 WhatsApp Business 帐户messages,然后发送测试消息以接收真实的负载,而无需进行部署。

WhatsApp Webhooks 使用两种不同的验证流程

最重要的区别是 Webhook 设置和 Webhook 传递的身份验证方式不同。在设置过程中,Meta 会发送 GET 请求,其中包含 hub.modehub.verify_tokenhub.challenge。您的端点比较验证令牌并以纯文本形式返回质询。之后,事件传递是POST请求;这些应该通过验证使用您的Meta 应用程序密钥创建的 HMAC 签名来进行身份验证。

验证令牌是您选择的随机字符串;它不是 WhatsApp 访问令牌,也不是应用程序密钥。返回质询证明了对回调端点的控制。它不会验证未来的 POST 请求。 Meta 的官方 WhatsApp webhook 指南 涵盖回调配置、订阅和 Webhook 字段。

创建 Next.js App Router 端点

下面的路由处理这两个阶段。使用 request.text() 读取 POST 数据会保留签名验证所需的确切字节。

// app/api/webhooks/whatsapp/route.ts
import crypto from 'node:crypto';
import { NextRequest } from 'next/server';

export const runtime = 'nodejs';

export async function GET(request: NextRequest) {
  const mode = request.nextUrl.searchParams.get('hub.mode');
  const token = request.nextUrl.searchParams.get('hub.verify_token');
  const challenge = request.nextUrl.searchParams.get('hub.challenge');

  if (mode === 'subscribe' && token === process.env.META_VERIFY_TOKEN) {
    return new Response(challenge ?? '', { status: 200 });
  }
  return new Response('Forbidden', { status: 403 });
}

export async function POST(request: Request) {
  const rawBody = await request.text();
  const supplied = request.headers.get('x-hub-signature-256') ?? '';
  const expected = 'sha256=' + crypto
    .createHmac('sha256', process.env.META_APP_SECRET!)
    .update(rawBody)
    .digest('hex');

  const a = Buffer.from(supplied);
  const b = Buffer.from(expected);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return new Response('Invalid signature', { status: 401 });
  }

  const payload = JSON.parse(rawBody);
  await enqueueWhatsAppPayload(payload);
  return new Response('EVENT_RECEIVED', { status: 200 });
}

不要调用 request.json() 然后为 HMAC 重建 JSON。空格、转义或键顺序可能会发生变化,从而产生不同的摘要。如果您使用 Express,请在全局 JSON 解析器之前捕获 Buffer。通用 webhook 签名指南解释了跨框架的原始正文处理。

启动隧道并配置回调

  1. 在本地运行 Next.js 应用程序,通常使用 npm run dev 在端口 3000 上运行。
  2. 运行npx portpreview 3000在单独的终端中。
  3. META_VERIFY_TOKEN设置为随机值,META_APP_SECRET设置为 Meta 应用程序设置中的应用程序密钥。
  4. 在 Meta 开发者仪表板中,打开 WhatsApp 产品的配置页面。
  5. 将回调 URL 设置为 https://YOUR-TUNNEL.portpreview.dev/api/webhooks/whatsapp 并输入相同的验证令牌。
  6. 验证成功后,订阅 WhatsApp Business 帐户的 messages 字段。

隧道在 GET 质询和后续 POST 传送期间必须保持活动状态。从旧会话复制的 URL 可能会解析,但不再转发到您的计算机,因此每当本地隧道发生更改时,请确认确切的回调。

独立测试 GET 质询

在使用仪表板之前,在本地重现请求:

curl -i \
  "http://localhost:3000/api/webhooks/whatsapp?hub.mode=subscribe&hub.verify_token=${META_VERIFY_TOKEN}&hub.challenge=123456"

正确的响应是带有正文的状态 200 123456,而不是 JSON,也不是 "123456" 带引号。如果令牌错误,则 403 是合适的。不要记录查询参数,因为验证令牌会出现在那里。

在编写业务逻辑之前了解消息有效负载

WhatsApp 将数据包装成几个深度级别。典型的通知包含 object: "whatsapp_business_account"、一个 entry 数组、一个 changes 数组以及一个 fieldmessages 的更改。在 value 内,传入的用户内容显示在 messages 中;您发送的邮件的送达、阅读和失败更新会显示在 statuses 中。

for (const entry of payload.entry ?? []) {
  for (const change of entry.changes ?? []) {
    if (change.field !== 'messages') continue;
    for (const message of change.value.messages ?? []) {
      await handleInboundMessage({
        id: message.id,
        from: message.from,
        type: message.type,
        text: message.text?.body,
      });
    }
    for (const status of change.value.statuses ?? []) {
      await updateDeliveryStatus(status.id, status.status);
    }
  }
}

不要假设每个通知都包含短信。图像、音频、文档、位置、交互式回复、系统消息和仅状态有效负载具有不同的形状。保持调度程序由 message.type 键入,验证可选字段,并保留未知事件类型以供审核而不是崩溃。

正确验证 POST 签名

X-Hub-Signature-256值使用表单sha256=<hex digest>。使用 Meta 应用程序密钥在原始请求字节上计算 HMAC-SHA256。永久或临时 WhatsApp 访问令牌用于 Graph API 调用;它不是 HMAC 密钥。使用恒定时间比较并拒绝丢失的签名。

保持启用本地验证。任何知道隧道 URL 的人都可以向其中 POST 任意 JSON。如果没有验证,伪造的事件可能会触发自动回复、改变 CRM 记录或暴露客户状态。如果意外提交、打印或共享应用程序密钥,则轮换应用程序密钥。

快速确认并删除重复消息

在对事件进行身份验证和持久排队后返回 200。下载媒体、致电法学硕士或更新多项服务时不要等待。当确认失败时,提供商会重试传送,并且网络模糊性意味着重复是正常的。

使用 WhatsApp 消息id作为入站消息和状态对象的幂等键。对已处理的 ID 施加唯一约束。状态转换可以合法地从已发送到已交付再到已读取,因此可以对每个相关转换进行重复数据删除,而不会丢弃后续状态。

对 WhatsApp Webhook 设置进行故障排除

无法验证回调 URL

通过公共 URL 测试 GET 路由。确保它接受 GET,比较确切的验证令牌,并仅使用质询进行响应。重定向、身份验证中间件、区域设置重写或 JSON 包装器可能会破坏验证。确认正在运行的开发进程已加载环境变量。

验证成功,但没有消息到达

仅回调验证不会将 WhatsApp Business 帐户订阅到字段。在仪表板中确认 messages 订阅以及电话号码属于预期的应用和帐户。如果应用仍处于开发模式,则从允许的收件人发送消息。

每个 POST 都无法签名验证

常见原因是使用访问令牌而不是应用程序密钥、对解析的 JSON 进行哈希处理、省略 sha256= 前缀或比较不同的编码。记录正文长度以及标头是否存在,但绝不打印秘密或完整的客户负载。

文本消息有效,但媒体处理失败

媒体通知包含 ID,不一定是文件字节。使用有效的访问令牌通过 Graph API 获取媒体,然后下载。将较慢的工作流程保留在 Webhook 确认路径之外。

本地端点会看到重复事件

检查响应状态和延迟,添加持久幂等性,并在每次修复后重播一个捕获的事件。 webhook 重播指南展示了如何避免为每次代码更改发送新的真实消息。

在本地测试期间保护客户数据

  • 尽可能使用测试电话号码和合成对话。
  • 编辑电话号码、消息正文、媒体 URL、联系人和个人资料日志中的名称。
  • 仅在忽略的环境文件或秘密管理器中存储应用程序密钥、访问令牌和验证令牌。
  • 限制谁可以查看隧道捕获并在调试会话后将其删除。
  • 在执行业务操作之前验证对象、字段和帐户标识符。

隧道使迭代速度更快,但它也带来了生产型个人信息数据到开发者机器。在与真实用户进行测试之前,应用隧道安全检查表中的控制。

常见问题

如何在本地主机上测试 WhatsApp Cloud API Webhook?
在本地运行您的 Webhook 处理程序,使用 HTTPS 隧道公开它,在 Meta 中注册公共回调 URL 并验证令牌,订阅消息,然后发送测试消息。
WhatsApp webhook 验证端点应返回什么?
对于 hub.mode 为 subscribe 且 hub.verify_token 匹配的有效 GET 请求,以 HTTP 200 以纯文本形式返回 hub.challenge 值。
如何验证 WhatsApp webhook POST 请求?
使用元应用程序密钥在确切的原始请求正文上计算 HMAC-SHA256,在十六进制摘要中添加 sha256= 前缀,并在时间上安全地将其与 X-Hub-Signature-256 进行比较。
为什么我经过验证的 WhatsApp Webhook 没有收到任何事件?
回调验证不会自动订阅每个字段。确认 WhatsApp Business 帐户已订阅消息,并且您的测试发件人和电话号码可供应用使用。