Webhookとは ― APIとの違いとイベント通知の仕組み
決済の
Webhookとは、出来事が起きたときに自動でHTTPリクエストを送る仕組み
GitHubの設定画面や決済サービスの管理画面には「Webhook URL」の欄があり、通知を受け取るプログラムのURLを入れます。Webhook(ウェブフック)とは、サービスの中で出来事が起きたときに、登録されたURLへ自動でHTTPリクエストを送る仕組みです。
決済が完了したら注文を確定する。コードがpushされたらテストを始める。障害が起きたらチャットに知らせる。こうした連携を、相手に何度も確かめに行かずに作れるので、サービスどうしをつなぐ場面でよく使います。その代わり、受け取るURLをインターネットに公開するため、偽の通知や同じ通知の重複に備える必要があります。
WebhookとAPIの違いは、どちらからリクエストを送るか
Web APIでは、自分のプログラムが知りたいときにリクエストを送り、結果をレスポンスで受け取ります。Webhookでは向きが逆になり、出来事が起きたサービスが、こちらのURLへリクエストを送ってきます。どちらもHTTPを使うので、違うのは通信の方式より、誰がいつ送り始めるかです。取りに行く形をプル型、届けてもらう形をプッシュ型と呼ぶこともあります。
| 観点 | APIを | Webhookで |
|---|---|---|
| 送り | 自分の | 出来事が |
| 送る | 自分が | サービスの |
| 用意する | リクエストを | インターネットから |
| 向く | 必要な | 出来事を |
ポーリングとの違いは、確かめに行く回数と気づくまでの遅れ
相手の変化をAPIだけで知るには、一定の間隔でAPIを呼んで確かめます。このやり方がポーリングです。1分おきなら1日に1,440回呼ぶことになり、その多くは変化なしで終わります。間隔を短くすれば早く気づけますが、呼ぶ回数はその分だけ増えます。
Webhookなら、知りたい出来事を最初に1回登録するだけで済みます。GitHubの文書も、APIを呼び続けるより規模が大きくなっても対応しやすく、ほぼリアルタイムに更新を受け取れると説明しています。
ただし、APIが不要になるわけではありません。GitHubの文書は、情報が一度だけ、またはたまにしか要らないときや、調べる対象が少ないときはAPIの方が向くとしています。相手がWebhookに対応していないときや、公開のURLを用意できないときも、ポーリングを選びます。
Webhookの仕組み:URLの登録から2xxの応答まで
通知を送るサービスを送る側、通知を受けるプログラムを受け取る側と呼ぶことにします。1と2の準備は最初の1回だけで、3と4は出来事のたびに繰り返します。
- 受け取る側が、通知を受け付けるURLを用意する
- 送る側の設定画面に、そのURLと受け取りたいイベントの種類を登録する
- 出来事が起きると、送る側がそのURLへPOSTリクエストを送る
- 受け取る側が、成功を表す2xxのステータスコードを返す
2xxの応答は、通知を受け取ったという合図です。200番台以外の応答や、応答が返らないときは、送る側は配信の失敗として扱います。Stripeは301や302などの転送の応答も失敗として扱うので、転送の無い最終のURLを登録します。
Webhook URLの欄に入れるのは、受け取る側のURL
GitHubや決済サービスでは、自分で用意したサーバのURLを入れます。Stripeの場合、登録できるのは公開されたHTTPSのURLです。
Slackのように、サービスの方がURLを発行する場合もあります。SlackのIncoming Webhooksでは、発行されたURLへJSONをPOSTすると、決めたチャンネルにメッセージが出ます。監視ツールなどの設定画面に貼るのはこのURLで、この場合はSlackが受け取る側です。どちらの形でも、URLの持ち主が通知を受け取ります。
Slackの文書は、このURLに秘密の情報が含まれるとして、公開のリポジトリなどに載せないよう注意しています。GitHubの文書も、登録するURLに機密の情報を入れないよう勧めています。
通知の中身はJSONで、イベントの種類と内容が入る
通知の本文には、JSONがよく使われます。次の例は、架空の決済サービスが支払いの完了を知らせるリクエストです。
POST /webhooks/payment HTTP/1.1
Host: shop.example.com
Content-Type: application/json
X-Example-Signature: sha256=3f1c9a7e...
{
"id": "evt_1001",
"type": "payment.succeeded",
"data": {
"order_id": "A-2048",
"amount": 3980
}
}typeはイベントの種類で、受け取る側はこの値を見て処理を分けます。idはイベントを見分ける値で、同じ通知が2回届いたことに気づくために使います。dataには出来事の内容が入り、この例では注文番号と金額です。
項目の名前や置き場所は、サービスごとに違います。GitHubはイベントの種類をX-GitHub-Eventヘッダ、イベントを見分ける値をX-GitHub-Deliveryヘッダに入れます。Stripeは本文のtypeとidに入れます。GitHubもStripeも、受け取るイベントは必要な種類だけに絞るよう勧めています。
通知に最小限の情報だけを入れるサービスもあります。Stripeのシンイベントには、イベントの種類と対象のIDなどしか入っていません。受け取った側がAPIを呼んで最新の状態を取りに行くので、WebhookとAPIを組み合わせた形になります。
受け取る側の注意:署名の検証、重複への備え、すぐに応答
署名を検証して、本物の通知か確かめる
受け取るURLは公開されているので、誰でもPOSTを送れます。偽の支払い完了の通知を信じると、代金を受け取っていない注文を確定してしまいます。Stripeの文書も、検証しなければ、攻撃者が偽のイベントで注文の処理などを実行できると警告しています。
GitHubやStripeでは、送る側と受け取る側だけが知る文字列を共有しておきます。この文字列をシークレットと呼びます。送る側は本文とシークレットから検査用の値を計算し、ヘッダに入れて送ります。この計算方法がHMACです。受け取る側は同じ計算をして、ヘッダの値と比べます。一致すれば、シークレットを知る相手が送った通知で、途中で書き換えられていないと分かります。
この値は署名と呼ばれていますが、公開鍵を使うデジタル署名ではなく、同じ鍵を両側で持つMACの一種です。GitHubはX-Hub-Signature-256ヘッダ、StripeはStripe-Signatureヘッダに値を入れます。どちらもSHA-256を使うHMACで計算します。検証するときは、次の点を守ります。
- 受け取った本文をそのまま計算に使う。JSONとして読み直した本文では、値が合わなくなることがある
- 比べるときは
==を使わず、かかる時間が内容で変わらない比較の関数を使う - シークレットはコードに書かず、環境変数などに置く
正しい通知を盗み見て、もう一度送りつける攻撃もあります。これをリプレイ攻撃と呼びます。Stripeは署名の対象に時刻を含めるので、攻撃者は時刻を書き換えられません。公式のライブラリは、既定で5分より古い通知を拒否します。
Stripeは署名の検証に加えて、送信元のIPアドレスを許可リストで絞る方法も勧めています。
再送と重複に備えて、同じ通知を2回処理しない
配信に失敗した通知の扱いは、サービスごとに違います。Stripeは本番環境で、間隔を広げながら最長3日間送り直します。GitHubは自動で再送しないので、失敗した配信は画面やAPIから手動で再送します。
再送があると、同じ通知が2回以上届くことがあります。処理は終わったのに、2xxの応答が送る側に届かなかった場合です。同じ処理を2回動かすと、商品を二重に発送したり、確認のメールを2通送ったりします。受け取る側は、同じ通知が何回届いても結果が変わらないように作ります。この性質を冪等性(べきとうせい)と呼びます。
RFC 9110はPOSTを冪等なメソッドに入れていないので、HTTPの決まりには頼れません。よくある作り方は、処理したイベントのIDを記録し、記録にあるIDが届いたら処理せずに2xxだけを返す方法です。GitHubのX-GitHub-Deliveryは、再送でも同じ値のままなので、この用途に使えます。
届く順番にも頼れません。Stripeは、イベントが発生順に届くことを保証しないと明記しています。順番が大事な処理では、通知の内容をそのまま使わず、APIで最新の状態を確かめます。
すぐに応答して、重い処理はあとで行う
送る側は、応答を長くは待ちません。GitHubは10秒以内に2xxを返すよう求めていて、遅いと接続を切って失敗として扱います。Stripeも、時間のかかる処理より先に2xxを返すよう求めています。
そこで、受信の処理では署名とイベントIDを確かめたら、仕事を処理待ちの列に入れて2xxを返します。この列をキューと呼びます。メールの送信や在庫の更新は、別のプログラムがキューから取り出して進めます。通知が一度に増えても、キューがあれば処理できる速さで順に片付けられます。
届いた通知ごとに、受け取る側の動きを比べる
受け取るプログラムの4つの段階を、順に並べました。場面によって、どこで止まり、どの応答を返すかが変わります。
- 通過署名を
確かめる ヘッダの値と 計算した 値が 一致 - 通過イベントIDを
確かめる 初めて届いた ID - 通過キューに
入れる 注文の確定を キューに 登録 - 通過応答を
返す すぐに返す
- 止める署名を
確かめる ヘッダの値と 計算した 値が 不一致 - 行わないイベントIDを
確かめる 中身を信用しないので 見ない - 行わないキューに
入れる 何も登録しない - 止める応答を
返す エラーを返す
- 通過署名を
確かめる ヘッダの値と 計算した 値が 一致 - 注意イベントIDを
確かめる 記録にある ID。 処理済み - 行わないキューに
入れる 二重の処理を 防ぐ ため入れない - 通過応答を
返す 受け取ったことだけ 伝える
- 通過署名を
確かめる ヘッダの値と 計算した 値が 一致 - 通過イベントIDを
確かめる 初めて届いた ID - 注意キューに
入れる 使わず、その 場で 15秒かけて 処理 - 止める応答を
返す 10秒の期限に 間に合わない
署名を最初に確かめるのは、偽の通知の中身を使わないためです。処理済みのIDに2xxを返すのは、失敗と判断されて再送が続くのを止めるためです。
Webhookがよく使われる場面:決済、CI、チャット通知
決済の完了を受けて注文を確定する
決済の中には、結果がすぐに決まらないものがあります。Stripeは、顧客の銀行が決済を確認したときや、継続の支払いが成功したときの例を挙げています。Webhookを受けていれば、利用者がブラウザを閉じたあとでも、サーバが結果を受け取って注文を確定できます。
コードのpushをきっかけにテストを始める
GitHubは、コードがpushされると、登録されたURLへ通知を送れます。これをCI/CDのツールが受けると、ビルドとテストのパイプラインが動き出します。開発者が手で実行しなくても、pushのたびに同じ検査が走ります。
監視の異常をチャットに知らせる
監視ツールやCIの結果は、チャットのWebhookへ送ると、担当者の見ているチャンネルに表示できます。前の節で見たとおり、チャットのサービスが発行したURLを、通知を送るツールに登録します。どの異常を通知するかの決め方は、オブザーバビリティの記事が参考になります。
クラウドでの受け口は、APIゲートウェイとサーバーレス関数の組み合わせ
受け口は普通のWebサーバでも作れますが、通知の来ない時間も動かし続けることになります。そこでクラウドでは、公開のURLを持つAPIゲートウェイと、リクエストが来たときだけ動くサーバーレスの関数を組み合わせることがあります。例として、AWSにはAmazon API GatewayとAWS Lambda、AzureにはAzure FunctionsのHTTPトリガーがあります。
サーバーレスの関数は、しばらく呼ばれないと起動に時間がかかることがあります。応答の期限に遅れないよう、関数の中では署名の検証とキューへの登録までに留めます。
Webhookを送る側を作るときの注意:SSRF
自分のサービスにWebhookの機能を付けると、利用者が登録したURLへサーバがリクエストを送ることになります。社内向けのアドレスやクラウドの管理用のアドレスを登録されると、外からは届かない場所へサーバを使って接続されます。この攻撃をSSRFと呼び、OWASPの資料もWebhookを例に挙げています。
送る前に、名前解決した先のアドレスが社内向けでないことを確かめ、確かめたそのアドレスへ接続します。確かめたあとに名前解決をやり直すと、別のアドレスへ向けられるおそれがあるからです。
参考にした資料
- GitHub Docs「About webhooks」
- GitHub Docs「Webhook events and payloads」
- GitHub Docs「Validating webhook deliveries」
- GitHub Docs「Best practices for using webhooks」
- GitHub Docs「Handling failed webhook deliveries」
- Stripe ドキュメント「Webhook エンドポイントで Stripe イベントを受信する」
- Stripe ドキュメント「イベントの仕組み」
- Slack Developer Docs「Sending messages using incoming webhooks」
- OWASP「Server Side Request Forgery Prevention Cheat Sheet」
- RFC 9110「HTTP Semantics」(9.2.2)
- RFC 2104「HMAC: Keyed-Hashing for Message Authentication」
- AWS Lambda Developer Guide「Invoking a Lambda Event Function using an Amazon API Gateway endpoint」
- Microsoft Learn「Azure Functions HTTP trigger」
練習問題(全6問)
正解:サービスの
Webhookでは、
正解:出来事が
Webhookは
正解:通知を
Webhookの
正解:第三者が
受け取る
正解:処理は
同じ
正解:署名を
送る
選んでから「採点する」を押すと、解説つきで答え合わせができます。