【ITニュース解説】Duplicate verification webhooks: deduplicate before you grant anything
2026年10月20日に「Dev.to」が公開したITニュース「Duplicate verification webhooks: deduplicate before you grant anything」について初心者にもわかりやすく解説しています。
ITニュース概要
ウェブフックでは同じイベントが重複し、権限の二重付与など問題が発生する。これを防ぐには、データベースで重複をチェックし、権限付与などの状態変更を一連の処理として実行することが重要だ。記事はPythonの例でその実装と課題を解説する。本番適用には署名検証など別途検討が必要。
ITニュース解説
現代の多くのシステムでは、インターネットを通じて異なるアプリケーションが互いに連携し合うことが不可欠だ。その際、あるイベントが発生したことを別のシステムに自動で通知する仕組みとして「Webhook(ウェブフック)」が広く使われている。例えば、ECサイトで注文が確定した際に、在庫管理システムや配送システムに「注文確定」というイベントを伝えるのがWebhookの一例である。しかし、この便利なWebhookには一つの注意すべき落とし穴がある。それは、同じイベントが二重に、あるいは複数回送られてくる可能性があるという問題だ。
Webhookの送信元システムは、イベントを通知した後、受信側からの応答を待つ。もし何らかの理由で応答がなかったり、予期せぬエラーが返ってきたりした場合、送信元は「イベントが正しく届かなかったかもしれない」と判断し、同じイベントをもう一度送ることがよくある。これは一般的な「リトライ(再試行)」と呼ばれる振る舞いだ。 システムエンジニアとしてこのようなWebhookを扱う際、この再試行が問題となるケースがある。特に、ユーザーのID認証や何らかの権限付与を行うシステムでは深刻な事態になりかねない。例えば、ユーザーがあるサービスへのアクセス権を付与されたというWebhookイベントが二重に届いた場合を考えてみよう。もしシステムがそのイベントを何も考えずに二回処理してしまうと、本来一回だけ付与されるべき権限が二重に記録されたり、あるいは以前の状態を更新すべきではないのに誤って上書きされたりする可能性がある。これはシステムの信頼性やセキュリティに直結する大きな問題となる。
このような重複イベントによる問題を避けるために、受信側のシステムは「重複排除(Deduplication)」という処理を行う必要がある。つまり、同じイベントが何度来ても、一度しか処理しないようにする仕組みだ。では、この重複排除はシステムのどこで、どのように実装すべきなのだろうか。 一般的な方法としては、イベントを一時的に保持する「キュー」や、高速なデータアクセスが可能な「キャッシュ」を使って、以前処理したイベントIDを記録しておく方法などが考えられる。しかし、この記事ではより堅牢なアプローチとして、「データベースのトランザクション内で、イベントの処理と同時に重複排除を行う」ことを強く推奨している。 「トランザクション」とは、複数の処理を一つのまとまった単位として扱い、その単位内のすべての処理が成功するか、あるいは一つでも失敗した場合はすべての処理を取り消す(ロールバックする)ことで、データの一貫性を保つ仕組みのことだ。銀行のATMでの預金引き出しをイメージすると分かりやすい。口座残高の確認、引き出し金額の減算、現金の払い出し、これらすべてが成功して初めて取引完了となる。もし途中で現金が詰まれば、引き出し金額の減算も取り消され、残高は元に戻る。 Webhookの重複排除においても、このトランザクションの考え方が非常に重要だ。イベントが重複していないかのチェック、イベント内容のデータベースへの記録、そしてそれに基づくシステムの「状態(ステータス)」の変更といった一連の処理を、一つのトランザクションとして実行することで、途中で何らかの障害が発生しても、データが中途半端な状態になることを防ぎ、同時に重複イベントによる意図しない状態変更も防ぐことができるのだ。
記事では、このトランザクションを使った重複排除の考え方を具体的に示すために、Python製の検証用サンプルコード(フィクスチャ)を例に挙げている。このサンプルでは、SQLiteという軽量なデータベースを使い、Webhookイベントがどのように処理されるかを示している。
まず、このフィクスチャの核となる部分として、イベントを記録するeventsテーブルに「(session, event)」という組み合わせを「主キー(Primary Key)」として設定している。主キーとは、そのテーブル内でレコードを一意に識別するための特別なカラム(またはカラムの組み合わせ)で、同じ値の組み合わせを複数登録できないという制約がある。これにより、「あるセッションに対して同じイベントIDは一度しか登録できない」というデータベースレベルでの重複排除が保証される。
イベントを受信するハンドラ(処理部分)は、常にBEGIN IMMEDIATEという特別なトランザクションを開始する。このトランザクション内で、以下の処理が行われる。
- 基本的なチェック: 受信したイベントが、特定のユーザー(テナント)や対象(サブジェクト)と、現在のセッション情報と一致するかを確認する。一致しない場合は、処理を拒否する。
- 重複イベントの検出と処理: イベントIDが既にデータベースの
eventsテーブルに存在するかどうかをチェックする。もし既に存在していれば、それは重複イベントであるため、データベースの状態を変更することなく、現在のシステムの状態をそのまま応答として返す。つまり、二度目の処理は行わない。 - 新規イベントの処理: イベントIDがデータベースに存在しない場合、それは新しいイベントであるため、
eventsテーブルにイベントを記録し、それに基づいてシステムの「ステータス(状態)」を更新する。このイベント記録とステータス更新は、すべて前述のトランザクション内で実行される。
このように、重複チェックと状態変更を一つのトランザクションにまとめることで、原子性(Atomic性)が保証される。原子性とは、そのトランザクション内のすべての操作が完全に実行されるか、あるいは全く実行されないかのどちらかであることを意味する。これにより、たとえ並行して同じイベントが複数回届いたとしても、データベースへの書き込みは一度しか行われず、システムの状態が意図せず二重に変わってしまうことを防げるのだ。
記事では、いくつかの具体的なシナリオを通じて、この重複排除の仕組みがどのように機能するかを説明している。
- 承認イベント(初回): ユーザーが承認されたというイベントが初めて届くと、システムはそれを新しいイベントとして記録し、ユーザーの権限を付与し、
grant_count(権限付与回数を記録するカウンター)を1とする。 - 承認イベント(重複): その後、全く同じ承認イベントが再度届いても、システムは既に同じイベントIDが記録されていることを検知する。結果として、データベースの状態は変更されず、
grant_countも1のまま維持される。二重に権限が付与されることはない。 - 拒否イベント: 次に、承認を「拒否」するという別のイベントが届いた場合、システムはそのイベントを新しいものとして処理し、ユーザーの権限を取り消す。この際、
grant_countは以前の履歴として1のまま保持されるが、permission_granted(権限付与フラグ)はfalseになる。 - 期限切れ: もし特定のイベントが定められた期限内に届かなかった場合、システムは自動的にそのセッションを期限切れとして扱い、権限を
falseにする。
これらのシナリオからわかるように、このアプローチはイベントの重複を確実に防ぎつつ、システムのライフサイクルに応じた状態変化を正しく管理できるよう設計されている。
このフィクスチャは重複排除の核となる部分を示しているが、実際のシステムでは他にも多くの要素を考慮する必要がある。例えば、イベントに不明なステータスが含まれていた場合、システムはそれを決して「承認」とは見なさず、権限を付与しない。これはセキュリティ上非常に重要だ。また、実際のWebhookでは、イベントが本当に正規の送信元から送られてきたものなのかを確認するために、「デジタル署名」の検証が不可欠である。この記事のフィクスチャは合成データしか扱わないため、実際の署名検証は実装されていないが、本番環境では必ず導入すべき重要な要件の一つだ。もしあなたがPHPのフレームワークであるLaravelを使って開発する場合、同様の重複排除を実現するためには、データベースのユニーク制約を使い、ステータス更新と同時にデータを書き込む方法が考えられる。また、署名検証や迅速な応答、そしてセッション管理なども独自に実装する必要がある。
Webhookイベントの重複処理は、特にID認証や権限管理といった重要なシステムにおいて、意図しないデータ不整合やセキュリティリスクを引き起こす可能性がある。この記事が示すように、重複排除の処理をデータベースのトランザクション内に組み込み、イベントの記録と状態変更を原子的に行うことで、この問題を堅牢に解決できる。これはシステムエンジニアとして、信頼性の高いシステムを構築する上で非常に重要な設計原則の一つと言える。このアプローチを参考に、あなたのシステムでも堅牢なWebhook処理を実装してみてほしい。