メインコンテンツにジャンプ

Web Hookの実践的な例: セキュアな実装ガイド

Node.js、Python、Goのための完全なWeb Hookの例を探します。 code で署名を検証し、リプレイ攻撃を防ぎ、エンドポイントをデバッグする方法を学びましょう。

Web Hookの実践的な例: セキュアな実装ガイド

サービスが何かが起こったときに反応する必要がある場合があります。支払いが確定した。顧客レコードが変更された。リポジトリがプッシュされた。 API を毎分ポーリングし、繰り返し「何か新しいものがあるか?」と尋ねるのではなく、ソースシステムがイベントが発生したときに呼び出すようにすることができます。

ほとんどのWeb Hookの例の記事はここで止まります。ルートを示し、JSONボディを印刷し、 200, して完成です。 そのバージョンは、正当な要求が送信される、有効な要求が再生される、またはフレームワークが署名検証前にボディを解析した場合に機能します。

このガイドでは、実際に使用するプロダクション用のパスを取っています。 例は小さくてコピーできるものの、重要な部分を含んでいます: raw body の処理、HMAC 検証、タイムスタンプのチェック、高速のアッセルメント、実用的なデバッグ。

目次

Webhookとは何か、どのように使用するか

請求先のサービスプロバイダーは、02:13に請求書を支払いとしてマークします。アプリが02:14にそれを知った場合、顧客はすぐにアクセスできます。アプリが次のポーリングサイクルでそれを知った場合、顧客は待ち、サポートチームはチケットを受け取り、ログは避けられるノイズで埋め尽くされます。Webhookはそのタイミング問題を解決するために、イベントが発生したときにHTTPコールバックを送信することでそれを解決します。

実際には、Webhookはイベント駆動型のPOSTです。プロバイダーは変更を検出し、例えば invoice.paid, order.createdpush、イベントデータを制御するURLに送信します。そのためには、定期的なポーリングが生み出す「何か新しいものがあるの?」のループを削除し、多くの無駄な要求を削減します。

実際のシステムでは、このパターンが現れます。なぜなら、ビジネスイベントに簡潔にマップできるからです。Stripeは支払い結果を投稿します。GitHubはリポジトリのアクティビティを投稿します。Shopifyは注文の更新を投稿します。形状は単純ですが、生産的な動作はそうではありません。金額、権限、または在庫を更新するWebhookには、リトライ、重複、信頼できないトラフィックが含まれる場合に、どのパブリックAPIエンドポイントと同じ注意を払う必要があります。

成功するためのメンタルモデル

Webhookフローをフレームする有用な方法は、4つの部分が協力して働いているように見ることです。

  • ソースシステムイベントを検出するサービス
  • 宛先エンドポイントWebhookを受信するHTTPルート
  • イベントイベント名が付いた変更、例えば invoice.paid または push.
  • ペイロード. The request body with the details your code needs.

既存のイベントについての事実を提供するプロバイダーが存在します。 そのプロバイダーを検証し、リクエストが最新であることを確認し、最後に変更を適用するというプロセスが重要です。 これは、多くの基本的なチュートリアルが認めるよりも多くの場合、生産環境では、重複配信は通常の動作であり、エッジケースではありません。

実用的なルール: イベント駆動型の更新にはWebhookを使用し、スケジュールされた読み取り、バックフィル、提供者がアウトバウンドイベントを提供しない場合にはポーリングを使用します。

より広範な ワークフロー自動化とデータ統合のチームが作業している場合、Webhookは通常、システムを同期させるイベント層として機能し、不要なリクエストトラフィックを回避します。 統合重視のサービスで作業している場合、Capgoの バックエンド開発記事 は、リトライ、キュー、観測性、エラー処理などのコア問題が再現される可能性があるため、有用なコンテキストです。

生産環境で機能するものと機能しないもの

生産環境で機能するものは、通常、デザイン上の面白みがなく、サブスクライブする必要があるイベントにのみサブスクライブし、エンドポイントをプロバイダーまたはイベントファミリーによってスコープすること、イベントIDを保存して重複配信が再現されることなく、リクエストが検証されキューに登録されたら、迅速に2xxのレスポンスを返し、次に遅いビジネスロジックを非同期に実行することなどが含まれます。

脆弱なバージョンは容易に認識できます。1つの汎用エンドポイントがすべてを処理します。署名チェックは早期テスト中にはスキップされ、戻ってこないのです。ハンドラーは、イベントが有効か古いものかを確認することなく、直接重要なテーブルに書き込むのです。その結果、デモでは機能しますが、リトライの嵐、プロバイダーの障害、または攻撃者が古い要求を再生する場合には失敗します。

そのトレードオフは、このガイドの残りの部分を定義します。ウェブホック受信者の「hello world」バージョンは小さく、生産環境に適したバージョンは、署名検証、再生防御、重複処理、デバッグ用のハックを最初から備えています。

ウェブホックのHTTP要求の構造

ウェブホックの要求を書く前に、codeを書く前に、HTTP要求をフレームワークのオブジェクトとしてではなく、raw HTTPとして見ることが役に立ちます。ウェブホックは一般的に、パブリックエンドポイントへのHTTP POST要求で、ヘッダーとJSONボディが含まれます。

シンプルなraw要求

POST /webhooks/orders HTTP/1.1
Host: your-app.example
Content-Type: application/json
User-Agent: Provider-Webhooks/1.0
X-Webhook-Signature: sha256=abc123example
X-Webhook-Timestamp: 1712345678

{
  "event": "order.created",
  "id": "evt_123",
  "data": {
    "order_id": "ord_456",
    "status": "created"
  }
}

重要な部分は簡単です:

  • メソッド実際には、ウェブホックの配信は通常POST要求です。
  • Content-Typeほとんどの現代のプロバイダーはJSONを送信します。
  • User-Agentデバッグに役立ちますが、信頼できるものではありません。
  • 署名ヘッダー. プロバイダーの有効性確認を含みます。
  • タイムスタンプヘッダー. 古いまたは再生された要求を拒否するために使用されます。

ボディーの形状がなぜ重要か

あなたのcodeは通常、すべてのフィールドに気にしない。 それがイベントのタイプ、イベントの識別子、ビジネスオブジェクトの中に気にします。 data. それがなぜ良いハンドラーは、必要なものだけをパースし、残りのものはトラブルシューティングのためにログする必要があるからです。

OpenAPIは、このパターンを直接モデル化しています。 OpenAPI 3.1.0は、トップレベルの webhooks オブジェクトを追加しました。 それぞれのウェブホックは、パスアイテムと同様に記述されますが、プロバイダによってトリガーされます。 canonical例では、 newPet ウェブホックの post オペレーション、JSON要求ボディー、受信を示す 200 レスポンスを含みます。 OpenAPI webhook example.

独自の受信者またはプロバイダーの契約をドキュメント化している場合、具体的な例は抽象的なスキーマの文章よりも効果的です。私は、次のような参照を使用することを好みます。 SheetMergyのAPIドキュメントの例 具体的な例、フィールドの説明、そして期待されるレスポンスがどのように組み合わさっているかが明確になるためです。

Webhookはトランスポート層では単純です。ほとんどのエラーはヘッダ、ボディのエンコード、または署名規則に関する誤った仮定によるものです。

Webhook署名の安全な検証方法

署名されたWebhookは1つの質問に答えます: そのペイロードは誰かが共有シークレットを知っている人から来ていますか?

これは、リクエストが最近であるか、またはすでに処理したことがあるかを尋ねることとは異なります。署名検証は最初のゲートであり、最後のゲートではありません。

Webhook署名の検証フローを示すイラストグラム

検証フロー

通常のHMACフローは次のようになります:

  1. プロバイダーのヘッダから署名を読み取ります。
  2. スタイルガイドを読みます raw request body 正確に受信したままです。
  3. セキュアな設定からウェブホックシークレットを読み込みください。
  4. 同様のアルゴリズムを使用して、予想されるHMACを再計算する。
  5. 受信の署名と計算された署名をタイミング安全に比較してください。
  6. リクエストを拒否するには、指定された値と一致しなければなりません。

JSON のボディを直接取得するステップは、ほとんどの場合、良好な実装が失敗するポイントです。フレームワークが JSON を先にパースしたり、スペースの整形やエンコードの詳細を変更したりする場合、計算された署名はサービスプロバイダーのものと一致しません。

What to watch for in real code

これらの最もよく見られる間違いです。

  • JSONをパースしたハッシュ. しないでください。 JSON.stringify(req.body) そして期待するのは一致すること。
  • 通常の文字列の等価性を使用します。タイミング安全な比較を使用します。
  • シークレットをハードコードする環境変数またはシークレットマネージャーに保持する。
  • ヘッダーだけに依存する署名ヘッダーは、検証しない限り意味がありません。

サービス間でシークレットの取り扱いを強化するチーム向けに、Capgoの APIのアプリストアの準拠性のための鍵のセキュリティのガイド は関連性があります。シークレットのローテーション、スコープ付きのアクセス、ログ内の漏洩を回避することは、ウェブホック受信者にとっても重要です。

汎用的な検証の例

const crypto = require('crypto');

function verifySignature(rawBody, receivedSignature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  const a = Buffer.from(receivedSignature, 'utf8');
  const b = Buffer.from(expected, 'utf8');

  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

これは意図的に汎用的なものです。実際のプロバイダーは、署名をプレフィックスする、タイムスタンプを署名内容に組み込む、またはハッシュを別の形式でエンコードすることがよくあります。ルールは同じです。プロバイダーの正確な署名形式に従い、常に元のペイロードと検証します。

脅威から保護する

署名付きのWebhookは、数時間後に到着し、ハンドラーがそれを新しいものとして扱う場合でも危険です。チームはそれが起こることを期待しています。プロキシはトラフィックを記録し、リクエストペイロードは間違った場所に漏れ出たり、プロバイダーはネットワーク障害後にリトライしたりし、エンドポイントは同じイベントを2回処理したりします。

Webアプリケーションにおけるリプレイ攻撃を効果的に防ぐための5つの重要なセキュリティ対策を示すチェックリスト。

署名検証は、共有シークレットでこのペイロードを作成したのは送信者だったかどうかを答える質問に答えます。リプレイ保護は別の質問に答えます: このリクエストは今すぐ受け入れるべきですか? 生産的な受信者には両方が必要です。

実際に重要な最小限のチェック

実用的リプレイ防御は署名付きのタイムスタンプから始まります。プロバイダーはヘッダーや署名付きメッセージにタイムスタンプを含め、受信者はリクエストが小さな許容範囲外にある場合にそれを拒否します。

そのフローは次のようになります:

  • プロバイダーが定義した場所からタイムスタンプを読み取りヘッダーの名前を推測しないでください。
  • RFC形式の日付として整数または日付としてパースしますプロバイダーの仕様に基づいて。
  • サーバーの時刻と比較します.
  • 古すぎるまたは将来のリクエストを拒否します。.
  • 署名スキームのうちタイムスタンプを検証します。 提供元が対応している場合。

最後の点は重要です。タイムスタンプが署名の範囲外にある場合、攻撃者は新しいタイムスタンプを挿入して元のボディを再生できます。私は常に提供元の正確な署名形式を確認することでタイムスタンプロジックを信頼します。

許容範囲の選択肢

5分は一般的なデフォルトです。攻撃ウィンドウを縮小する程度に短く、時刻のずれや通常のネットワーク遅延を乗り切る程度に長いです。

ここではトレードオフが発生します。30秒のウィンドウはより安全に見えますが、実システムではリトライ、キューング、地域遅延などが絡むと頻繁に破綻します。30分のウィンドウは操作が容易ですが、署名されたリクエストが漏洩した場合に攻撃者に与える時間が長くなります。最初は数分、サーバーをNTPと同期し、提供元の配信パターンが対応する場合にのみ許容範囲を絞り込んでください。

リプレイ防御は単にタイムスタンプの検証ではありません。

タイムスタンプ検証は古いリクエストをブロックしますが、有効なウィンドウ内で同じ署名されたイベントが 2 回送信された場合、処理を認識するには別のレイヤーが必要です。

2 番目のレイヤーを使用します。

  • イベント ID または配信 ID を短期間のストレージである Redis にトラッキングします。 in a short-lived store such as Redis.
  • ハンドラを idempotent とみなす したがって、繰り返し配信は、重複した注文、メール、請求アクションを生成しないようにする。
  • 古いリクエストを拒否する 理由コードとともに、しかし、秘密情報やフルな敏感なペイロードをログに記録しないようにする。
  • 検証とキューの重い作業を他の場所で行う後、迅速なレスポンスを返す。 すでに有効期限切れのウィンドウや取り消しを考慮しているチームは、このパターンを認識するだろう。

Capgoのガイド Capacitorアプリケーションにおけるトークン取り消しパターン 有効期限切れのトークンは、有効期限切れではないトークンと同じように扱うべきではない。

署名されたものでも古いものでも、安全ではない。

Node.js で Webhook 受信者を作る

Node と Express を使うと、真剣な受信者をオンラインにしたりするのが一番速いが、ある罠が他のすべてのものよりも重要である。Express がオブジェクトに変換する前に、raw body にアクセスする必要がある。

ノートパソコンが木製の机の上で、Node.js 受信機 code を表示しているVS Code エディター環境。

実用的なExpressの例

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

const app = express();
const PORT = process.env.PORT || 3000;
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

// Capture raw body for signature verification
app.use(
  express.json({
    verify: (req, res, buf) => {
      req.rawBody = buf;
    },
  })
);

function safeEqual(a, b) {
  const aBuf = Buffer.from(a, 'utf8');
  const bBuf = Buffer.from(b, 'utf8');
  if (aBuf.length !== bBuf.length) return false;
  return crypto.timingSafeEqual(aBuf, bBuf);
}

function verifySignature(rawBody, secret, receivedSignature) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  return safeEqual(expected, receivedSignature);
}

function isFresh(timestampHeader, toleranceSeconds = 300) {
  const timestamp = Number(timestampHeader);
  if (!Number.isFinite(timestamp)) return false;

  const now = Math.floor(Date.now() / 1000);
  return Math.abs(now - timestamp) <= toleranceSeconds;
}

app.post('/webhooks/example', async (req, res) => {
  const signature = req.get('x-webhook-signature');
  const timestamp = req.get('x-webhook-timestamp');

  if (!WEBHOOK_SECRET) {
    return res.status(500).send('Webhook secret is not configured');
  }

  if (!signature || !timestamp) {
    return res.status(400).send('Missing required security headers');
  }

  if (!isFresh(timestamp)) {
    return res.status(401).send('Stale webhook');
  }

  const valid = verifySignature(req.rawBody, WEBHOOK_SECRET, signature);
  if (!valid) {
    return res.status(401).send('Invalid signature');
  }

  // Acknowledge quickly
  res.status(200).send('OK');

  // Process after acknowledgement
  try {
    const event = req.body;
    console.log('Accepted event:', event.event, event.id);
    // enqueueJob(event)
  } catch (err) {
    console.error('Post-ack processing failed:', err);
  }
});

app.listen(PORT, () => {
  console.log(`Webhook receiver listening on ${PORT}`);
});

この構造が持続する理由

ここではいくつかの選択肢が意図的に行われました:

  • Raw Bodyキャプチャはミドルウェアで発生します。元のバイトをハッシュするために保存するため。
  • タイムスタンプはビジネスロジックの前にチェックされます。古いトラフィックに対しての作業は無駄です。
  • ルートは 200 すぐに戻ります。。長時間の作業はキューまたはバックグラウンドタスクに属するものです。
  • ポストアック処理は分離されています. 連続したロジックが失敗しても、受信パスは小さくなる。

多くのウェブホック実装の弱点はシークレットです。ソースに保管しないでください、テスト用のフィクスチャに貼り付けてはいけません、ログに反映してはいけません。CI/CDパイプラインのシークレットの管理についての Capgo のガイドが、運用面をよくカバーしているので、必要な場合はそれを参照してください。 シークレットの管理 シークレットの管理

シークレットの管理

シークレットの管理

実際のシステムでは変更すること

実際のプロバイダーの統合では、イベントIDの重複排除を永続ストレージに追加する、リクエストIDを含む構造化ログを追加する、承認パスの後ろにキューを追加するなど、実際のシステムでは変更することです。また、複数のプロバイダーが異なる署名形式を使用する場合、単一の汎用エンドポイントを避け、分離されたハンドラーを使用することで、ハンドラーを簡単に理解し、簡単に破壊することができるようにします。

PythonでWebhook受信者を作成する

Flaskは、明確なリクエストハンドリングと、HMACのためのPythonの標準ライブラリが用意されているため、ウェブホックのクリーンな例として適しています。

Nodeと同じように、主なことは、raw request bytesと比較して、パースされたJSON dictを検証することです。

import os
import time
import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "")

def is_fresh(timestamp_header, tolerance_seconds=300):
    try:
        timestamp = int(timestamp_header)
    except (TypeError, ValueError):
        return False

    now = int(time.time())
    return abs(now - timestamp) <= tolerance_seconds

def verify_signature(raw_body, secret, received_signature):
    expected = hmac.new(
        secret.encode("utf-8"),
        raw_body,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected, received_signature)

@app.route("/webhooks/example", methods=["POST"])
def webhook():
    if not WEBHOOK_SECRET:
        return "Webhook secret is not configured", 500

    signature = request.headers.get("X-Webhook-Signature")
    timestamp = request.headers.get("X-Webhook-Timestamp")

    if not signature or not timestamp:
        return "Missing required security headers", 400

    if not is_fresh(timestamp):
        return "Stale webhook", 401

    raw_body = request.get_data()

    if not verify_signature(raw_body, WEBHOOK_SECRET, signature):
        return "Invalid signature", 401

    payload = request.get_json(silent=True) or {}

    # Acknowledge receipt
    response = jsonify({"status": "ok"})

    # In production, queue payload here instead of heavy sync work
    print("Accepted event:", payload.get("event"), payload.get("id"))

    return response, 200

if __name__ == "__main__":
    app.run(port=5000, debug=True)

署名とタイムスタンプのチェックを含むFlaskの例

request.get_data() ここでは、呼び出しに必要なのはキーだけです。 これは、ボディのrawバイトを提供します。 すぐにでもボディのrawバイトにジャンプすると、署名の不一致が混乱を招くようになります。 request.json署名の不一致が混乱を招くようになったら、もうすぐです。

実装に関するいくつかの注意点があります。

  • 使用する代わりに平等の代わりに使用してください。 hmac.compare_digest ヘッダが欠けている場合、クライアントの失敗として扱い、早期に拒否します。
  • JSONのパースに使用する代わりに使用してください。 エラー処理を制御したい場合は、Flaskが例外を上げるのではなく、代わりに使用してください。
  • ルートを薄く保ちます。 silent=True . 付加的なコストを引き起こす可能性のあるペイロードがトリガーをトリガーした場合、作業をキューに追加します。 __CAPGO_KEEP_0__
  • __CAPGO_KEEP_0____CAPGO_KEEP_0__

セキュリティチェックを緩和してサインミスマッチをデバッグしないでください。 その代わりに、ハッシュしたバイトを正確に印刷し、プロバイダーが期待するフォーマットを正確に印刷してください。

チームがいつも詰まる場所

通常の失敗パスは、手作りのJSONボディでテストし、実際のプロバイダーに切り替えてサインが一致しないことを発見することです。 これは、通常、次の3つのことのいずれかです: プロバイダーがタイムスタンプ付きのエンベロープを署名する、署名が想定どおりにエンコードされていない、またはミドルウェアがボディを検証する前に変更した。

その場合、ランダムに暗号 code を変更しないでください。 その代わりに、RAWヘッダーとRAWボディをキャプチャし、ハッシュを小さな孤立したスクリプトで再現し、そして最後にFlaskルートに戻してください。

GoでWebhook受信者を構築する

GoはWebhook受信者としての素晴らしい選択肢です。標準ライブラリは十分です。 フレームワークを使用する必要はなく、小さく信頼できるハンドラーを取得し、 code を正確に管理するのは簡単です。

ボディハンドリングに注意する必要があるのは1つだけです。 r.Body ストリームです。 1回だけ読み取り、取得したバイトをハッシュし、そして同じバイトからアンマーシャルしてください。

標準ライブラリの例

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/hex"
	"encoding/json"
	"io"
	"log"
	"net/http"
	"os"
	"strconv"
	"time"
)

type WebhookPayload struct {
	Event string          `json:"event"`
	ID    string          `json:"id"`
	Data  json.RawMessage `json:"data"`
}

func isFresh(timestampHeader string, toleranceSeconds int64) bool {
	ts, err := strconv.ParseInt(timestampHeader, 10, 64)
	if err != nil {
		return false
	}

	now := time.Now().Unix()
	diff := now - ts
	if diff < 0 {
		diff = -diff
	}

	return diff <= toleranceSeconds
}

func verifySignature(rawBody []byte, secret string, received string) bool {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(rawBody)
	expected := hex.EncodeToString(mac.Sum(nil))

	if len(expected) != len(received) {
		return false
	}

	return subtle.ConstantTimeCompare([]byte(expected), []byte(received)) == 1
}

func webhookHandler(secret string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
			return
		}

		signature := r.Header.Get("X-Webhook-Signature")
		timestamp := r.Header.Get("X-Webhook-Timestamp")

		if signature == "" || timestamp == "" {
			http.Error(w, "missing required security headers", http.StatusBadRequest)
			return
		}

		if !isFresh(timestamp, 300) {
			http.Error(w, "stale webhook", http.StatusUnauthorized)
			return
		}

		rawBody, err := io.ReadAll(r.Body)
		if err != nil {
			http.Error(w, "failed to read body", http.StatusBadRequest)
			return
		}

		if !verifySignature(rawBody, secret, signature) {
			http.Error(w, "invalid signature", http.StatusUnauthorized)
			return
		}

		var payload WebhookPayload
		if err := json.Unmarshal(rawBody, &payload); err != nil {
			http.Error(w, "invalid json", http.StatusBadRequest)
			return
		}

		w.WriteHeader(http.StatusOK)
		w.Write([]byte("OK"))

		log.Printf("accepted event=%s id=%s", payload.Event, payload.ID)
	}
}

func main() {
	secret := os.Getenv("WEBHOOK_SECRET")
	if secret == "" {
		log.Fatal("WEBHOOK_SECRET is not set")
	}

	http.HandleFunc("/webhooks/example", webhookHandler(secret))

	log.Println("listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Goがここで堅実な理由

いくつかの利点が目立っています:

  • ハンドラーは明確です. No hidden middleware magic.
  • エッジでタイプが役立ちます。. ヘッダー解析、タイムスタンプ変換、JSONデコードはすべて明確に失敗します。
  • 標準の暗号化パッケージは十分です。. 基本的なHMAC検証用の追加依存関係は必要ありません。

運用に関する注記

ウェブフックの量が増えると、Goの並行性モデルはHTTPエントリポイントを変更することなくバックグラウンドワークを拡散させるための余裕を与えます。 その場合でも、受信者は狭くしてください。 受け取る、検証する、承認する、そして受信者に引き渡す。

Goのウェブフックハンドラの中で最も強力なものは、つり合いがとれたものです。 それらは、運輸検証とビジネスロジックを混ぜ合わせないし、レスポンスが戻る前にデータベースの重い作業を行わない。

基本的なデバッグテクニック

ウェブフックのバグは通常、スタックトレースではなくサポートメッセージとして表示されます。 プロバイダーはイベントを配信したと言い、エンドポイントはアプリに何も到達しなかった、または最初から見てみれば有効なように見えるリクエストで署名検証が失敗したと言います。 その時点で、デバッグは、バイトごとに、正確なHTTP交換を再構築し、どこで破れたかを証明することです。

ウェブフックをデバッグするための5つの基本的なツールとテクニックのリスト。

実用的なデバッグツールキット

Wire形式から始めましょう。

署名検証が失敗した場合、元のリクエストボディと検証に使用されたヘッダーを、元のまま受信したものとともにキャプチャします。実際には、問題はよくありません。フレームワークはJSONをパースした後、ハッシュしたり、プロキシはエンコードを変更したり、テスト再生では元のタイムスタンプヘッダーをミスしたりします。パースされたオブジェクトをログするだけでは十分ではありません。元のバイトと検証入力が必要です。

これらのツールは、問題を迅速に分離するのに役立ちます:

  • リクエストキャプチャ調査中はヘッダー、コンテンツタイプ、コンテンツ長、未修正ボディをログします。
  • リクエスト検査エンドポイントサービスは、送信者が送信したものを確認するのに役立ちます。 webhook.site ローカルトンネリング
  • と同様のツールは、プロバイダーをループさせながらローカル受信者とテストできます。. ngrok 手動再生
  • リクエストを再構築します。__CAPGO_KEEP_0__ curl または、Postmanを使用して同じボディとヘッダーで確認することができます。 これが、codeまたはプロバイダーのペイロードが問題であるかどうかを確認する最速の方法です。
  • プロバイダーの配信ログパターンは重要です。外から内側へ作業してください。最初に、プロバイダーが期待どおりに送信したかどうかを確認してください。次に、サーバーが同じバイトを受信したかどうかを確認してください。最後に、__CAPGO_KEEP_0__が同じバイトを同じシークレットとタイムスタンプのルールでハッシュしたかどうかを確認してください。

The pattern matters. Work from the outside in. First verify the provider sent what you expected. Then verify your server received the same bytes. Then verify your code hashed the same bytes with the same secret and timestamp rules.

良いウェブホックログは、1つの検索で3つの質問に答えるべきです。

質問

役立つログフィールド リクエストが到着したか?
ルート、メソッド、受信日時 なぜ却下された?
欠落ヘッダー、古いタイムスタンプ、署名失敗 missing_header, stale_timestamp, signature_failed
後で関連付けられますか? event_id, provider_request_id

実際のシステムでは、4番目のフィールドが役立ちます。ローカル request_id 生成された受信者の方が、リクエストをアプリ、キュー、ワーカーログを通して追跡できます。

保存するものを選択してください。秘密をログに記録しないでください。顧客データ、アクセストークン、請求情報を含むフルプロダクションペイロードをダンプしないでください。代わりに、メタデータと短いボディハッシュをログに記録してください。リトライと、2つの配信が同一であるかどうかを検証するために、比較が可能です。

元の入力とともに、エラーの再現を行ってください。

基本的なチュートリアルでは、この部分を省略します。失敗したリクエストを完全に再現できない場合は、推測していることになります。

失敗したウェブホックを保存してください:

  • 元のボディのバイト
  • 署名に関連するすべてのヘッダー
  • リクエストタイムスタンプ
  • コンテンツタイプ
  • プロバイダーのリクエストID

次に、ステージングエンドポイントに再生してみましょう。再生が成功した場合、トランジットで何が変化したかを比較してみましょう。一般的な原因には、リクエストボディを正規化するミドルウェア、文字エンコーディングの不一致、ヘッダを削除または書き換えるロードバランサが含まれます。私は、チームが実際のリクエストボディではなく、プリティプリントされたダッシュボードビューからペイロードをコピーしたことによる失敗も見ました。ホワイトスペースの差異だけでHMAC検証を破ることができました。

より広範なリリースとモバイルトランスポートのトラブルシューティングのために、同じデバッグの規範がCapgoのガイドに現れます。 Capacitor異なるトランスポートでも同じ教訓です。実際のリクエストパスをキャプチャして、適用する前にアプリケーションcodeを変更しましょう。

署名検証が失敗した場合、rawバイト、検証に使用したexactヘッダ、タイムスタンプ値を確認して、暗号化codeを触る前にみましょう。

生産用Webhookのチェックリスト

ステージングではWebhookハンドラーはほとんど問題なく見えますが、最初のリトライの嵐、不正なペイロード、2時ごろの署名不一致などで問題が発生するまで。生産環境では標準が高くなります。受信者は偽のリクエストを拒否し、有効なリトライを許可し、エラーのデバッグに必要なシグナルを提供しながら、敏感なデータを公開しないようにします。

セキュリティと正しさのチェック

  • すべてのリクエスト署名を検証エンドポイントURLは漏洩します。テストURLはチャットで共有されます。署名検証は、共有シークレットを知っている送信者がいることを示す制御です。
  • 古いリクエストを拒否. 旧のペイロードに署名された有効な署名は、再送信されることができます。 これを防ぐには、プロバイダーのリトライモデルに合わせたタイムスタンプの許容範囲を設定してください。
  • JSONをパースしたものではなく、raw bodyをハッシュしてください。. ミドルウェアはキーを並べ替える、ホワイトスペースを正規化する、またはエンコードを変更することができます。 したがって、到着したexact bytesに対して検証を実行する必要があります。
  • .codeから署名シークレットを取り出してください。. 環境変数は基本的なものです。 ただし、クレデンシャルを定期的にローテートする場合や、複数の環境で実行する場合など、シークレットマネージャーはより適切な選択肢です。
  • 認証エラーの場合、失敗するようにしてください。.署名ヘッダが欠落している、不正確な、または予想外のスキームを使用している場合、リクエストを拒否し、理由をログに記録してください。

信頼性のチェック

  • 速いことを認識してください。. プロバイダは通常、2xxを成功として扱います。 したがって、リクエストを検証し、必要なものを保存し、後続の作業をキューまたはワーカーに移行してください。
  • ハンドラーをidempotentにします。. 同じイベントは複数回到着する可能性があります。 したがって、イベントID、配信ID、または安定したプロバイダ識別子を使用して、側効果をイベントに分離してください。
  • 予測可能なエラー コードを返します。. 不正入力の場合を考慮してください。 400 . または 4014035xx
  • . Accept only the fields and event types you support. Loose parsing feels convenient at first and becomes expensive during provider API changes.

標準を使用してください:

  • 受信、検証、処理を別々の結果として追跡する.
  • リクエストID、イベントID、署名ステータス、タイムスタンプスキューをログに記録する.
  • キュー遅延、ハンドララテンシー、リトライボリュームを測定する.
  • ステージングまたは再送信ワークフロー用に安全な再送信パスを維持する.
  • パターン変更に警告する例として、署名失敗のスパイクや重複配信など

Capgoは、より広範な運用ポイントの例として便利です。更新ワークフローには、リリース配信と観察性のツールが含まれ、エコシステムの部分もウェブホック関連フローに触れています。実践的な教訓です。配信システムには、受信から完了までの可視性が必要です。

チームが上記のチェックをカバーしている場合、ウェブホック受信者は通常、生産用に良好な状態になります。どのアイテムが欠けている場合、その欠陥はデモ中に発生するのではなく、インシデント中に発生します。

ウェブホックに関するよくある質問

何のステータスcodeを返すべきですか?

ステータス__CAPGO_KEEP_0__を返す 2xx ウェブホックを受け入れると、検証が失敗した場合、エラーを返します。エラーは、失敗の原因に応じて、例えば 400 入力が不正である場合 401 認証データが不正である場合

検証ロジックを一貫して実装することで、プロバイダーのダッシュボードをより簡単に理解できます。

ウェブホックを同期的に処理するべきですか?

通常は、ウェブホックを検証し、承認し、実際の作業をキューまたはバックグラウンドワーカーに送信するようにします。これにより、配信パスが速くなり、遅延した下流処理によってトリガーされる重複リトライが減ります。

リトライをどのように扱うべきですか?

リトライが発生することを前提として、ハンドラーに無害性を実装してください。同じイベントを受信しても、副作用が重複しないようにします。イベントIDやプロバイダーの配信IDは、通常のアンカーです。

イベントが順序が乱れた場合どうしますか?

順序を許容できるハンドラーを設計することができます。ビジネスプロセスがシーケンスを必要とする場合、古いトランジションを検出するために十分な状態を保存するのではなく、配信順序がイベント順序を反映していると仮定しないでください。

ウェブホックのバージョン変更にどう対処するべきですか?


If your team ships Capacitor or Electron apps, Capgo Capgoは、署名されたWeb更新を配布するための制御された方法を提供し、ロールアウトの動作を観察し、インシデントから回復するための方法を提供します。これにより、App Storeのレビューを待つことなく、Webhookの設計の同じエンジニアリングの本能に沿った方法で、入力の検証、リリースパスの観察、回復の高速化が可能になります。

リアルタイムの更新はCapacitorアプリに

ウェブ層のバグが生じた場合、Capgoを通じて修正を配信するのではなく、数日間待ってアプリストアの承認を待つのではなく、ユーザーはバックグラウンドで更新を受け取り、ネイティブの変更は通常のレビュー経路を通じて

マーティンによる人間のサポート

今すぐ始めましょう

最新のブログ記事

Capgoは、プロフェッショナルなモバイルアプリを作成するために必要な最良の洞察を提供します。