# 在 Webhook 端点中接收 Stripe 事件 在您的 Webhook 端点监听来自 Stripe 的事件,即可让您的集成自动触发响应。 ## Start here: Install agent skills Stripe recommends using official skills to integrate with the API using best practices. Follow the instructions below to learn how. **Tip: Use curl instead of Fetch tools.** Skills are detailed 10–20 KB markdown files. Fetch tools summarize them. `curl -sL` guarantees the full content. **Using `npx skills`**: Check if `npx` is installed on the system. If `npx` is available, run `npx skills add https://docs.stripe.com --list` to list available skills, then run `npx skills add https://docs.stripe.com --yes --skill <--skill other-skills>` to install individual skills. **If `npx skills` isn’t available**: You can use curl to fetch the index of available skills, what they do, and their files from `https://docs.stripe.com/.well-known/skills/index.json`. To download a skill and its related files, use curl to download them from `https://docs.stripe.com/.well-known/skills/`. For this article, we recommend using these skills: `stripe-best-practices` > #### 将事件发送到您的 AWS 账户或 Azure 订阅 > > 您可以将事件直接发送到 [Amazon EventBridge](https://docs.stripe.com/event-destinations/eventbridge.md) 或 [Azure Event Grid](https://docs.stripe.com/event-destinations/eventgrid.md) 作为事件接收端。 创建一个事件接收端,以接收 HTTPS Webhook 端点上的事件。在您注册 Webhook 端点后,当您的 Stripe 账户有事件发生时,Stripe 可以将实时[事件](https://docs.stripe.com/event-destinations.md#events-overview)数据推送到您的应用程序的 Webhook 端点。Stripe 用 HTTPS 将 Webhook 事件以包含[事件对象](https://docs.stripe.com/api/events.md)的 JSON 有效载荷发送到您的应用程序。 接收 Webhook 事件可帮助您响应异步事件,如客户银行确认付款、客户对收款提出异议或经常性付款成功。 ## 开始 要开始在您的应用程序中接收 Webhook 事件: 1. 创建一个 Webhook 端点处理程序来接收事件数据的 POST 请求。 1. 使用 Stripe CLI 在本地测试您的 Webhook 端点处理程序。 1. 为 Webhook 端点创建新的[事件接收端](https://docs.stripe.com/event-destinations.md)。 1. 保护您的 Webhook 端点。 您可以注册并创建一个端点,用它同时处理几个不同的事件类型,也可以为特定事件设置单独的端点。 ## 组织事件接收端不支持的事件类型行为 Stripe 会异步发送大多数事件类型,但对于某些事件类型,会等待响应。在这些情况下,Stripe 会根据事件接收端是否响应而采取不同的处理方式。 如果您的事件接收端接收[组织](https://docs.stripe.com/get-started/account/orgs.md)事件,需要响应的事件有以下限制: - 您无法订阅组织接收端的 `issuing_authorization.request`。相反,请在组织内的 Stripe 账户中设置一个 [Webhook 端点](https://docs.stripe.com/webhooks.md#example-endpoint)来订阅此事件类型。使用 `issuing_authorization.request` 实时授权购买请求。 - 当您在网站中直接嵌入 [Checkout](https://docs.stripe.com/payments/checkout.md) 或将客户重定向到 Stripe 托管的支付页面时,接收 `checkout_sessions.completed` 的组织目标无法[处理重定向行为](https://docs.stripe.com/checkout/fulfillment.md#redirect-hosted-checkout)。要控制 Checkout 重定向行为,请使用组织内某个 Stripe 账户中配置的 [Webhook 端点](https://docs.stripe.com/webhooks.md#example-endpoint)处理此事件类型。 - 如果组织目标对 `invoice.created` 事件响应失败,在使用自动收款的情况下,无法影响[账单自动定稿](https://docs.stripe.com/billing/subscriptions/webhooks.md#understand)。您须使用组织内某个 Stripe 账户中配置的 [Webhook 端点](https://docs.stripe.com/webhooks.md#example-endpoint)处理此事件类型,才能触发账单自动定稿。 ## 创建处理程序 设置一个 HTTP 或 HTTPS 端点函数,该函数可以用 POST 方法接受 Webhook 请求。如果您仍在您的本地机器上开发您的端点,则它可以使用 HTTP。可公开访问后,您的 Webhook 端点函数必须使用 HTTPS。 设置您的端点函数,以便它: - 使用由[事件对象](https://docs.stripe.com/api/events/object.md)组成的 JSON 有效载荷处理 POST 请求。 - 对于[组织](https://docs.stripe.com/get-started/account/orgs.md)事件处理程序,Stripe 会检查`上下文`值,确定组织中哪个账户生成了此事件,随后设置与`上下文`值相对应的 `Stripe-Context` 标头。 - 在执行任何可能导致超时的复杂逻辑之前,快速返回一个成功状态码 (`2xx`)。例如,您必须返回一个 `200` 响应,然后才能在您的会计系统中将客户的账单更新为已支付。 > - 借助我们的[交互式 Webhook 端点构建器](https://docs.stripe.com/webhooks/quickstart.md),使用您的编程语言构建一个 Webhook 端点函数。 - 请使用 Stripe API 参考文件,以确定您的 Webhook 处理器需要处理的 [精简事件对象](https://docs.stripe.com/api/v2/core/events/event-types.md)或[快照事件对象](https://docs.stripe.com/api/events/object.md)。 #### 示例端点 此代码片段是一个 Webhook 函数,配置它的目的是检查从 Stripe 账户接收的事件、处理事件并返回 `200` 响应。使用 API v1 资源时引用[快照](https://docs.stripe.com/event-destinations.md#events-overview)事件处理程序,使用 API v2 资源时引用[精简](https://docs.stripe.com/event-destinations.md#events-overview)事件处理程序。 #### 快照事件处理程序 创建快照事件处理程序时,通过访问事件的 `data.object` 字段,在事件发生时为您的逻辑使用 API 对象定义。您还可以从 Stripe API 中检索 API 资源,以访问最近最新的对象定义。 #### Ruby ```ruby require 'json' # Replace this endpoint secret with your unique endpoint secret key # If you're testing with the CLI, run 'stripe listen' to find the secret key # If you defined your endpoint using the API or the Dashboard, check your webhook settings for your endpoint secret: https://dashboard.stripe.com/webhooks endpoint_secret = 'whsec_...'; # Using Sinatra post '/webhook' do payload = request.body.read event = nil begin event = Stripe::Event.construct_from( JSON.parse(payload, symbolize_names: true) ) rescue JSON::ParserError => e # Invalid payload status 400 return end # Check that you have configured webhook signing if endpoint_secret # Retrieve the event by verifying the signature using the raw body and the endpoint secret signature = request.env['HTTP_STRIPE_SIGNATURE']; begin event = Stripe::Webhook.construct_event( payload, signature, endpoint_secret ) rescue Stripe::SignatureVerificationError => e puts "⚠️ Webhook signature verification failed. #{e.message}" status 400 end end # Handle the event case event.type when 'payment_intent.succeeded' payment_intent = event.data.object # contains a Stripe::PaymentIntent # Then define and call a method to handle the successful payment intent. # handle_payment_intent_succeeded(payment_intent) when 'payment_method.attached' payment_method = event.data.object # contains a Stripe::PaymentMethod # Then define and call a method to handle the successful attachment of a PaymentMethod. # handle_payment_method_attached(payment_method) # ... handle other event types else puts "Unhandled event type: #{event.type}" end status 200 end ``` #### 精简事件处理程序(Clover+ 版) 创建精简事件处理程序时,请使用 `fetchRelatedObject()` 方法检索与事件关联对象的最新版本。事件可能包含需要通过 `EventNotification` 上的 `.fetchEvent()` 实例方法才能获取的[附加数据](https://docs.stripe.com/event-destinations.md#fetch-data)。该等数据的具体结构取决于`事件类型`。 事件类型必须在版本发布时完成部署,才能确保其在该版本 SDK 中生成对应类。如需处理 SDK 未预置对应类的事件,请使用 `UnknownEventNotification` 类进行承接。 #### Python ```python import os from stripe import StripeClient from stripe.events import UnknownEventNotification from flask import Flask, request, jsonify app = Flask(__name__) api_key = os.environ.get("STRIPE_API_KEY", "") webhook_secret = os.environ.get("WEBHOOK_SECRET", "") client = StripeClient(api_key) @app.route("/webhook", methods=["POST"]) def webhook(): webhook_body = request.data sig_header = request.headers.get("Stripe-Signature") try: event_notif = client.parse_event_notification( webhook_body, sig_header, webhook_secret ) # type checkers will narrow the type based on the `type` property if event_notif.type == "v1.billing.meter.error_report_triggered": # in this block, event_notification is typed as # a V1BillingMeterErrorReportTriggeredEventNotification # there's basic info about the related object in the notification print(f"Meter w/ id {event_notif.related_object.id} had a problem") # or you can fetch the full object form the API for more details meter = event_notif.fetch_related_object() print( f"Meter {meter.display_name} ({meter.id}) had a problem" ) # And you can always fetch the full event: event = event_notif.fetch_event() print(f"More info: {event.data.developer_message_summary}") elif event_notif.type == "v1.billing.meter.no_meter_found": # in this block, event_notification is typed as # a V1BillingMeterNoMeterFoundEventNotification # that class doesn't define `fetch_related_object` because the event # has no related object. # so this line would correctly give a type error: # meter = event_notif.fetch_related_object() # but fetching the event always works: event = event_notif.fetch_event() print( f"Err! No meter found: {event.data.developer_message_summary}" ) # Events that were introduced after this SDK version release are # represented as `UnknownEventNotification`s. # They're valid, the SDK just doesn't have corresponding classes for them. # You must match on the "type" property instead. elif isinstance(event_notif, UnknownEventNotification): # these lines are optional, but will give you more accurate typing in this block from typing import cast event_notif = cast(UnknownEventNotification, event_notif) # continue matching on the type property # from this point on, the `related_object` property _may_ be None # (depending on the event type) if event_notif.type == "some.new.event": # if this event type has a related object, you can fetch it obj = event_notif.fetch_related_object() # otherwise, `obj` will just be `None` print(f"Related object: {obj}") # you can still fetch the full event, but it will be untyped event = event_notif.fetch_event() print(f"New event: {event.data}") # type: ignore return jsonify(success=True), 200 except Exception as e: return jsonify(error=str(e)), 400 ``` #### 精简事件处理程序(金合欢版或罗勒版) 创建精简事件处理程序时,请使用 `fetchRelatedObject()` 方法检索与事件关联的对象的最新版本。精简事件可能包含只能使用 API 检索的[其他上下文数据](https://docs.stripe.com/event-destinations.md#fetch-data)。使用带有精简事件 ID 的 `retrieve()` 调用来访问这些额外的有效载荷字段。 #### Python ```python import os from stripe import StripeClient from stripe.events import V1BillingMeterErrorReportTriggeredEvent from flask import Flask, request, jsonify app = Flask(__name__) api_key = os.environ.get('STRIPE_API_KEY') webhook_secret = os.environ.get('WEBHOOK_SECRET') client = StripeClient(api_key) @app.route('/webhook', methods=['POST']) def webhook(): webhook_body = request.data sig_header = request.headers.get('Stripe-Signature') try: thin_event = client.parse_thin_event(webhook_body, sig_header, webhook_secret) # Fetch the event data to understand the failure event = client.v2.core.events.retrieve(thin_event.id) if isinstance(event, V1BillingMeterErrorReportTriggeredEvent): meter = event.fetch_related_object() meter_id = meter.id # Record the failures and alert your team # Add your logic here return jsonify(success=True), 200 except Exception as e: return jsonify(error=str(e)), 400 if __name__ == '__main__': app.run(port=4242) ``` #### 使用 `context` #### 快照事件 此代码片段是一个 Webhook 函数,配置它的目的是检查接收的事件、检测原始账户(适当时)、处理事件并返回 `200` 响应。 #### Ruby ```ruby require 'json' # Using Sinatra post '/webhook' do payload = request.body.read event = nil begin event = Stripe::Event.construct_from( JSON.parse(payload, symbolize_names: true) ) rescue JSON::ParserError => e # Invalid payload status 400 return end # Extract the context context = event.context # Define your API key variables (ideally loaded securely) ACCOUNT_123_API_KEY = "sk_test_123" ACCOUNT_456_API_KEY = "sk_test_456" account_api_keys = { "account_123" => ACCOUNT_123_API_KEY, "account_456" => ACCOUNT_456_API_KEY } api_key = account_api_keys[context] if api_key.nil? puts "No API key found for context: #{context}" status 400 return end # Handle the event case event.type when 'customer.created' customer = event.data.object begin latest_customer = Stripe::Customer.retrieve( customer.id, { api_key: api_key } ) handle_customer_created(latest_customer, context) rescue => e puts "Error retrieving customer: #{e.message}" status 500 return end when 'payment_method.attached' payment_method = event.data.object begin latest_payment_method = Stripe::PaymentMethod.retrieve( payment_method.id, { api_key: api_key } ) handle_payment_method_attached(latest_payment_method, context) rescue => e puts "Error retrieving payment method: #{e.message}" status 500 return end else puts "Unhandled event type: #{event.type}" end status 200 end ``` #### 精简事件处理程序(Clover+ 版) 使用 `EventNotification` 的 `context` 属性来确定 [organization](https://docs.stripe.com/get-started/account/orgs.md) 内事件所对应的账户。除 `.fetchRelatedObject()` 和 `.fetchEvent()` 两个方法会自动设置外,其余所有 API 调用均需手动设置 [Stripe-Context 标头](https://docs.stripe.com/context.md)。 #### Python ```python org_api_key = os.environ.get("STRIPE_API_KEY") webhook_secret = os.environ.get("WEBHOOK_SECRET") client = StripeClient(org_api_key) # inside your webhook handler event_notification = client.parse_event_notification(payload, sig_header, webhook_secret) # uses `context` automatically event_notification.fetch_event() # pass context manually for other API requests client.v1.invoices.list(stripe_context=event_notification.context) ``` #### 精简事件处理程序(金合欢版或罗勒版) 此代码片段是一个 Webhook 函数,用于接收整个组织内的轻量级事件、验证签名、通过 `context` 字段确定事件来源账户,并在后续 API 调用中使用该账户的 API 密钥。 #### Python ```python import os from flask import Flask, request, jsonify from stripe import StripeClient from stripe.events import V1BillingMeterErrorReportTriggeredEvent app = Flask(__name__) org_api_key = os.environ.get("STRIPE_API_KEY") webhook_secret = os.environ.get("WEBHOOK_SECRET") client = StripeClient(org_api_key) account_api_keys = { "account_123": os.environ.get("ACCOUNT_123_API_KEY"), "account_456": os.environ.get("ACCOUNT_456_API_KEY"), } @app.route("/webhook", methods=["POST"]) def webhook(): payload = request.data sig_header = request.headers.get("Stripe-Signature") try: thin_event = client.parse_thin_event(payload, sig_header, webhook_secret) # Retrieve the event using the org client to inspect context event = client.v2.core.events.retrieve(thin_event.id) context = getattr(event, "context", None) if not context: return jsonify(error="Missing context"), 400 account_key = account_api_keys.get(context) if not account_key: return jsonify(error="Unknown context"), 400 account_client = StripeClient(account_key) full_event = account_client.v2.core.events.retrieve(thin_event.id) if isinstance(full_event, V1BillingMeterErrorReportTriggeredEvent): meter = full_event.fetch_related_object() meter_id = meter.id # Record the failures and alert your team # Add your logic here return jsonify(success=True), 200 except Exception as e: return jsonify(error=str(e)), 400 if __name__ == "__main__": app.run(port=4242) ``` ## 测试您的处理程序 在您使用 Webhook 端点函数之前,我们建议您测试您的应用程序集成情况。方法是配置本地侦听器,以此将事件发送到您的本地机器,并发送测试事件。测试时,您需要使用 [CLI](https://docs.stripe.com/stripe-cli.md)。 #### 将事件转发到本地端点 要将事件转发到本地端点,请使用 [CLI](https://docs.stripe.com/stripe-cli.md) 运行以下命令来设置本地侦听器。在[沙盒环境](https://docs.stripe.com/sandboxes.md)中,`--forward-to` 标志会将所有的 [Stripe 事件](https://docs.stripe.com/cli/trigger#trigger-event)发送到您本地的 Webhook 端点。请使用下面的相应 CLI 命令,具体取决于您使用的是[精简](https://docs.stripe.com/event-destinations.md#events-overview)还是快照事件。 #### 转发快照事件 使用以下命令将[快照事件](https://docs.stripe.com/event-destinations.md#events-overview)转发到本地侦听器。 ```bash stripe listen --forward-to localhost:4242/webhook ``` #### 转发精简事件 使用以下命令将[精简事件](https://docs.stripe.com/event-destinations.md#events-overview)转发到本地侦听器。 ```bash $ stripe listen --forward-thin-to localhost:4242/webhook --thin-events "*" ``` > 您也可以运行 `stripe listen` 命令在 [Stripe Shell](https://docs.stripe.com/workbench/shell.md) 中查看事件,但无法将事件从 Shell 转发到您的本地端点。 有些配置有助于用本地侦听器进行测试,这些配置包括: - 禁用 HTTPS 证书验证,使用 `--skip-verify` 可选标志。 - 仅转发特定事件时,使用 `--events` 可选标志,并传入事件列表,用逗号分隔事件。 #### 转发目标快照事件 使用以下命令将目标快照事件转发到本地侦听器。 ```bash stripe listen --events payment_intent.created,customer.created,payment_intent.succeeded,checkout.session.completed,payment_intent.payment_failed \ --forward-to localhost:4242/webhook ``` #### 转发目标精简事件 使用以下命令将目标精简事件转发到本地侦听器。 ```bash stripe listen --thin-events v1.billing.meter.error_report_triggered,v1.billing.meter.no_meter_found \ --forward-thin-to localhost:4242/webhook ``` - 要将事件从已经在 Stripe 上注册的公共 Webhook 端点转发到本地 Webhook 端点,请使用 `--load-from-webhooks-api` 可选标志。它会加载您注册的端点,解析路径及其注册的事件,然后将路径附加到您本地的 Webhook 端点的 `--forward-to path` 路径中。 #### 从公共 Webhook 端点转发快照事件 使用以下命令将快照事件从公共 Webhook 端点转发到本地侦听器。 ```bash stripe listen --load-from-webhooks-api --forward-to localhost:4242/webhook ``` #### 从公共 Webhook 端点转发精简事件 使用以下命令将精简事件从公共 Webhook 端点转发到本地侦听器。 ```bash stripe listen --load-from-webhooks-api --forward-thin-to localhost:4242/webhook ``` - 要检查 Webhook 签名,请使用侦听命令初始输出中的 `{{WEBHOOK_SIGNING_SECRET}}`。 ```output Ready! Your webhook signing secret is '{{WEBHOOK_SIGNING_SECRET}}' (^C to quit) ``` #### 触发测试事件 要发送测试事件,请通过 Stripe 管理平台手动创建对象来触发事件接收端订阅的事件类型。了解如何用 [Stripe for VS Code](https://docs.stripe.com/stripe-vscode.md) 触发事件。 #### 触发快照事件 您可在 [Stripe Shell](https://docs.stripe.com/workbench/shell.md) 或 [Stripe CLI](https://docs.stripe.com/stripe-cli.md) 中使用以下命令。本示例会触发 `payment_intent.succeeded` 事件: ```bash stripe trigger payment_intent.succeeded Running fixture for: payment_intent Trigger succeeded! Check dashboard for event details. ``` #### 触发精简事件 您可以在 [Stripe CLI](https://docs.stripe.com/stripe-cli.md) 中使用以下命令。此示例会触发一个 `v1.billing.meter.error_report_triggered` 事件: ```bash stripe trigger v1.billing.meter.error_report_triggered Setting up fixture for: list_billing_meters Running fixture for: list_billing_meters Setting up fixture for: billing_meter Running fixture for: billing_meter Setting up fixture for: list_billing_meters_after_creation Running fixture for: list_billing_meters_after_creation Setting up fixture for: billing_meter_event_session Running fixture for: billing_meter_event_session Setting up fixture for: create_billing_meter_event_stream Running fixture for: create_billing_meter_event_stream Trigger succeeded! Check dashboard for event details. ``` ## 注册您的端点 测试 Webhook 端点函数后,使用 Workbench 中的 [API](https://docs.stripe.com/api/v2/event-destinations.md) 或 **Webhook** 选项卡注册 Webhook 端点的可访问 URL,以便 Stripe 知道将事件传递到何处。您最多可以在 Stripe 上注册 16 个 Webhook 端点。注册的 Webhook 端点必须是可公开访问的 **HTTPS** URL。 #### Webhook URL 格式 注册 Webhook 端点时使用的 URL 格式: ``` https:/// ``` 例如,如果您的域名是 `https://mycompanysite.com`,您的 Webhook 端点的路径为 `@app.route('/stripe_webhooks', methods=['POST'])`,那便指定 `https://mycompanysite.com/stripe_webhooks` 作为**端点 URL**。 #### 为 Webhook 端点创建事件接收端 在管理平台中用 Workbench 创建事件接收端,或通过[API](https://docs.stripe.com/api/v2/event-destinations.md) 程序化地创建。最多可以在每个 Stripe 账户上注册 16 个事件接收端。 #### 管理平台 在管理平台中创建新的 Webhook 端点: 1. 打开 Workbench 中的 [Webhook](https://dashboard.stripe.com/webhooks) 选项卡。 1. 点击**创建事件目的地**。 1. 选择从哪里接收事件。Stripe 支持两种类型的配置:**您的账户**和 [Connect 子账户](https://docs.stripe.com/connect.md)。选择**账户**,以从您自己的账户侦听事件。如果您创建了 [Connect 应用程序](https://docs.stripe.com/connect.md),并且想要侦听来自您的 Connect 子账户的事件,请选择 **Connect 子账户**。 > #### 侦听来自组织 Webhook 端点的事件 > > 如果您在[组织账户](https://docs.stripe.com/get-started/account/orgs.md)中创建 Webhook 端点,请选择**账户**以侦听来自您组织的账户的事件。如果您拥有作为组织成员的 [Connect 平台](https://docs.stripe.com/connect.md),并且想要侦听来自所有平台的 Connect 子账户的事件,请选择 **Connect 子账户**。 1. 为您想要使用的[事件对象](https://docs.stripe.com/api/events.md)选择 API 版本。 1. 选择要发送到 Webhook 端点的[事件类型](https://docs.stripe.com/api/events/types.md)。 1. 选择**继续**,然后选择 **Webhook 端点**作为接收端类型。 1. 点击**继续**,然后提供**端点 URL** 和 Webhook 的可选描述。 #### API 您可以创建一个新的事件接收端,它会在触发[用量计费](https://docs.stripe.com/billing/subscriptions/usage-based.md)验证错误时用 [API](https://docs.stripe.com/api/v2/event-destinations.md) 通知您。 如果您已创建[Connect 应用](https://docs.stripe.com/connect.md)并希望监听关联账户的事件,请使用 [events_from](https://docs.stripe.com/api/v2/core/event-destinations/create.md#v2_create_event_destinations-events_from) 参数并将其值设置为 `@accounts`。对于 [组织](https://docs.stripe.com/get-started/account/orgs.md)事件接收端,请使用 `@organization_members` 来接收来自您组织内账户的事件,或使用 `@organization_members/@accounts` 来接收来自您组织内所有 Connect 子账户的事件。 ```curl curl -X POST https://api.stripe.com/v2/core/event_destinations \ -H "Authorization: Bearer <>" \ -H "Stripe-Version: 2026-03-25.preview" \ --json '{ "name": "My event destination", "description": "This is my event destination, I like it a lot", "type": "webhook_endpoint", "event_payload": "thin", "enabled_events": [ "v1.billing.meter.error_report_triggered" ], "webhook_endpoint": { "url": "https://example.com/my/webhook/endpoint" } }' ``` > [Workbench](https://docs.stripe.com/workbench.md) 将替换掉当前的[开发人员管理平台](https://docs.stripe.com/development/dashboard.md)。如果您仍在使用开发人员管理平台,请查看如何[创建新的 Webhook 端点](https://docs.stripe.com/development/dashboard/webhooks.md)。 ## 保护您的端点 在确认您的端点可以按照预期那样工作后,通过实施 [Webhook 最佳做法](https://docs.stripe.com/webhooks.md#best-practices)来保护它。 确保您的处理程序验证所有 Webhook 请求均由 Stripe 生成,以此保障集成安全。您可使用官方库验证或手动验证 Webhook 签名。 #### 用官方库验证(推荐) ### 用官方库验证 Webhook 签名 我们建议使用我们的官方库来验证签名。通过提供事件的有效载荷、`Stripe-Signature` 头和端点的私钥来执行验证。如果验证失败,您会收到一个错误。 如果您收到签名验证错误,请阅读我们的指南中的[故障排除](https://docs.stripe.com/webhooks/signature.md)。 > Stripe 需要原始请求正文来执行签名验证。如果您使用的是一个框架,则要确保它不会篡改原始正文。对原始请求正文的任何篡改都会导致验证失败。 #### Ruby ```ruby # Don't put any keys in code. See https://docs.stripe.com/keys-best-practices. # Find your keys at https://dashboard.stripe.com/apikeys. Stripe.api_key = '<>' require 'stripe' require 'sinatra' # If you are testing your webhook locally with the Stripe CLI you # can find the endpoint's secret by running `stripe listen` # Otherwise, find your endpoint's secret in your webhook settings in # the Developer Dashboardendpoint_secret = 'whsec_...' # Using the Sinatra framework set :port, 4242 post '/my/webhook/url' do payload = request.body.readsig_header = request.env['HTTP_STRIPE_SIGNATURE'] event = nil beginevent = Stripe::Webhook.construct_event( payload, sig_header, endpoint_secret ) rescue JSON::ParserError => e # Invalid payload puts "Error parsing payload: #{e.message}" status 400 return rescue Stripe::SignatureVerificationError => e# Invalid signature puts "Error verifying webhook signature: #{e.message}" status 400 return end # Handle the event case event.type when 'payment_intent.succeeded' payment_intent = event.data.object # contains a Stripe::PaymentIntent puts 'PaymentIntent was successful!' when 'payment_method.attached' payment_method = event.data.object # contains a Stripe::PaymentMethod puts 'PaymentMethod was attached to a Customer!' # ... handle other event types else puts "Unhandled event type: #{event.type}" end status 200 end ``` #### 手动验证 ### 手动验证 Webhook 签名 虽然我们建议用我们的官方库来验证 Webhook 事件签名,但您仍可按照该部分的说明创建自定义解决方案。 每个签名事件中包含的 `Stripe-Signature` 头都有一个时间戳和一个或多个签名,必须对其进行验证。时间戳有一个 `t=` 前缀,每个签名都有一个 *scheme* 前缀。方案以 `v` 开头,后跟一个整数。目前,唯一有效的真实签名方案是 `v1`。为辅助测试,Stripe 会为测试模式下的事件发送一个使用虚假 `v0` 方案的附加签名。 ``` Stripe-Signature: t=1492774577, v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd, v0=6ffbb59b2300aae63f272406069a9788598b792a944a07aba816edb039989a39 ``` > 为清晰起见,我们已经添加了新行,而真正的 `Stripe-Signature` 头在单独一行。 Stripe 用基于散列的消息验证码([HMAC](https://en.wikipedia.org/wiki/Hash-based_message_authentication_code))和 [SHA-256](https://en.wikipedia.org/wiki/SHA-2)生成签名。为防止[降级攻击 (Downgrade attack)](https://en.wikipedia.org/wiki/Downgrade_attack),请忽略所有不是 `v1` 的方案。 当您[滚动某个端点的密钥](https://docs.stripe.com/webhooks.md#roll-endpoint-secrets)时,您可以有具有相同方案和密钥对的多个签名,并且可以使前一个密钥的活动状态保持长达 24 小时。在此期间,您的端点会有多个有效的密钥,Stripe 则为每个密钥生成一个签名。 要创建手动的签名验证解决方案,您必须完成以下步骤: #### 第 1 步:从标头提取时间戳和签名 拆分标头,用 `,` 字符作为分隔符,获得元素列表。然后分割每个元素,使用 `=` 字符作为分隔符,获得前缀和值对。 前缀 `t` 的值对应于时间戳,`v1` 对应于签名(一个或多个)。您可以丢弃所有其他元素。 #### 第 2 步:准备 `signed_payload` 字符串 `signed_payload` 字符串是通过连接以下内容创建的: - 时间戳(字符串形式) - 字符 `.` - 实际的 JSON 有效载荷(即请求体) #### 第 3 步:确定期望的签名 用 SHA256 散列函数计算 HMAC。用端点的签名密钥作为密钥,同时用 `signed_payload` 字符串作为消息。 #### 第 4 步:对比签名 将头内的签名与期望的签名进行比较。为进行相等匹配,应计算当前时间戳和收到的时间戳之间的差异,然后确定它是否在您的容差范围内。 为了防止定时攻击,请用恒定时间字符串比较方式将期望的签名与收到的每个签名进行比较。 ## 调试 Webhook 集成 向 Webhook 端点发送事件时,可能会出现多种问题: - Stripe 可能无法将事件发送到您的 Webhook 端点。 - 您的 Webhook 端点可能存在 SSL 问题。 - 您的网络连接时断时续。 - 您的 Webhook 端点没有接收到您期望接收的事件。 ### 查看事件的发送情况 要查看事件交付,请在 **Webhooks** 下选择 Webhook 端点,然后选择**事件**选项卡。**事件**选项卡提供事件列表及其状态:`已送达`、`待处理`或者`失败`。点击事件可查看元数据,包括交付尝试的 HTTP 状态码及未来待处理交付的时间。 您还可以用 [Stripe CLI](https://docs.stripe.com/stripe-cli.md) 直接在您的终端内[侦听事件](https://docs.stripe.com/webhooks.md#test-webhook)。 ### 修复 HTTP 状态代码 当一个事件显示状态代码 `200` 时,它表示已成功地发送到 Webhook 端点。您可能还会收到一个不同于 `200` 的状态代码。有关常见 HTTP 状态代码和推荐解决方案的列表,请查看下表。 | 待处理的 Webhook 状态 | 描述 | 修复 | | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | | (无法连接)ERR | 我们无法建立到目标服务器的连接。 | 确保您的主机域名可以公开访问互联网。 | | (`302`) ERR(或其他 `3xx` 状态) | 目标服务器尝试将请求重定向到另一个位置。这种情况,我们认为对 Webhook 请求的重定向响应是失败的。 | 将 Webhook 端点目的地设置为重定向解析的 URL。 | | (`400`) ERR(或其他 `4xx` 状态) | 目标服务器不能或不会处理请求。当服务器检测到错误 (`400`)、目标 URL 有访问限制 (`401`, `403`) 或目标 URL 不存在 (`404`) 时,可能会发生这种情况。 | - 确保您的端点可以公开访问互联网。 - 确保您的端点接受 POST HTTP 方法。 | | (`500`) ERR(或其他 `5xx` 状态) | 目标服务器在处理请求时遇到了错误。 | 查看应用程序的日志,了解它为什么返回 `500` 错误。 | | (TLS 错误)错误 | 我们无法与目标服务器建立安全连接。目标服务器的证书链中的 SSL/TLS 证书或中间证书的问题通常会导致这些错误。Stripe 需要 `v1.2` 或更高版本的 *TLS* (TLS refers to the process of securely transmitting data between the client—the app or browser that your customer is using—and your server. This was originally performed using the SSL (Secure Sockets Layer) protocol)。 | 执行 [SSL 服务器测试](https://www.ssllabs.com/ssltest/),找出可能导致此错误的问题。 | | (超时) 错误 | 目标服务器响应 Webhook 请求的时间过长。 | 务必要在您的 Webhook 处理代码中延迟复杂的逻辑并立即返回成功的响应。 | ## 事件的发送行为 本节旨在帮助您理解 Stripe 向 Webhook 端点发送事件时的不同行为。 ### 自动重试 在真实模式下,我们会尽量连续三天一直尝试将事件发送到您的接收端,但尝试频率递减。在事件接收端的**事件发送**选项卡中查看下次重试的时间(如果适用)。我们在几个小时内重试了三次在沙盒中创建的事件发送操作。如果在我们重新尝试时您的接收端已被禁用或删除,我们将阻止您再次尝试该事件。但是,如果您在我们重试之前禁用并又重新启用事件接收端,则仍可以看到今后的重试尝试。 ### 手动重试 有两种方法可以手动重试事件: - 在 Stripe 管理平台中,查看特定事件时点击**重新发送**。这适用于创建事件后的 15 天内。 - 用 [Stripe CLI](https://docs.stripe.com/cli/events/resend) 运行 `stripe events resend --webhook-endpoint=` 命令。这适用于创建事件后的 30 天内。 手动向 Webhook 端点重新发送之前发送失败的事件并不会解除 Stripe 的[自动重试行为](https://docs.stripe.com/webhooks.md#automatic-retries),即使返回的是 `2xx` 状态码。了解如何[处理未送达的 Webhook 事件](https://docs.stripe.com/webhooks/process-undelivered-events.md)以避免后续重试。 ### 事件排序 Stripe 不保证事件按照生成的顺序发送。例如,创建订阅时可能会生成以下事件: - `customer.subscription.created` - `invoice.created` - `invoice.paid` - `charge.created`(如果有收款) 确保您的事件接收端不依赖于以特定顺序接收事件。准备好适当管理他们的提交操作。您还可以使用 API 来检索任何缺少的对象。例如,如果您先收到此事件,可以使用 `invoice.paid` 中的信息检索账单、收款和订阅对象。 ### API 版本控制 事件发生时您的账户设置中的 API 版本表示 API 版本,因此会将 [Event](https://docs.stripe.com/api/events.md) 的结构发送到您的目的地。例如,如果您的账户设置为旧的 API 版本,如 2015-02-16,并且您通过 [versioning](https://docs.stripe.com/api.md#versioning) 更改特定请求的 API 版本,则生成并发送到您的接收端的 [Event](https://docs.stripe.com/api/events.md) 对象仍然是基于这个 2015-02-16 API 版本。[Event](https://docs.stripe.com/api/events.md) 对象一经创建即不能修改。例如,如果您更新某笔收款,则原始收款事件保持不变。因此,对您的账户 API 版本的后续更新不会追溯更改现有的 [Event](https://docs.stripe.com/api/events.md) 对象。通过较新版本的 API 调用 `/v1/events` 来获取旧的 [Event](https://docs.stripe.com/api/events.md) 对收到的事件的结构也没有影响。您可以将测试事件接收端设置到默认的 API 版本或最新的 API 版本。发送到接收端的 [Event](https://docs.stripe.com/api/events.md) 是针对接收端的指定版本而构建的。 ## Webhook 用法最佳实践 查看这些最佳做法,时刻确保您的 Webhook 安全,并与您的集成应用良好运行。 ### 处理重复事件 Webhook 端点有时可能会多次收到同一事件。您可以通过记录已处理的[事件 ID](https://docs.stripe.com/api/events/object.md#event_object-id),然后不处理已记录的事件来防止接收重复事件。 在某些情况下,会生成并发送两个单独的 Event 对象。要识别这些重复项,请使用 `data.object` 中对象 ID 和 `event.type`。 ### 仅侦听集成所需的事件类型 将 Webhook 端点配置为仅接收集成所需的事件类型。侦听额外的事件(或所有事件)会给您的服务器带来过度的压力,因此不建议这样做。 您可以在管理平台或通过 API [更改事件](https://docs.stripe.com/api/webhook_endpoints/update.md#update_webhook_endpoint-enabled_events)(Webhook 端点收到的事件)。 ### 异步处理事件 配置您的处理程序来处理带有异步队列的传入事件。如果选择同步处理事件,可能会遇到扩展性问题。Webhook 发送量的任何大峰值(例如,在所有订阅都续订的月初)都可能会使您的端点主机不堪重负。 异步队列可让您以系统支持的速度处理并发事件。 ### 去除 Webhook 路由的 CSRF 保护 如果您正在使用 Rails、Django 或其他网络框架,则您的站点可能会自动检查每个 POST 请求是否包含一个_CSRF 令牌_。这是一个重要的安全功能,有助于保护您及您的用户免受[跨站请求伪造 (cross-site request forgery, CSRF)](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_\(CSRF\)) 攻击。但是,这一安全措施也可能会阻止您的站点处理合法事件。如果是这样,则您可能需要从 CSRF 保护中去除 Webhook 路由。 #### Rails ```ruby class StripeController < ApplicationController # If your controller accepts requests other than Stripe webhooks, # you'll probably want to use `protect_from_forgery` to add CSRF # protection for your application. But don't forget to exempt # your webhook route! protect_from_forgery except: :webhook def webhook # Process webhook data in `params` end end ``` ### 通过 HTTPS 服务器接收事件 如果您为您的 Webhook 端点使用了 HTTPS URL(真实模式下要求),则 Stripe 会在发送您的 Webhook 数据之前先验证到您的服务器的连接是否安全。为此,您的服务器必须要正确配置,具有支持 HTTPS 的有效服务器证书。Stripe Webhook 仅支持 *TLS* (TLS refers to the process of securely transmitting data between the client—the app or browser that your customer is using—and your server. This was originally performed using the SSL (Secure Sockets Layer) protocol) 版本 v1.2 和 v1.3。 ### 定期滚动端点签名密钥 用于验证事件来源于 Stripe 的私钥可在 Workbench 的 **Webhook**选项卡中中修改。为了保证它们的安全,建议您定期或者在怀疑私钥泄露时滚动(更改)私钥。 要滚动私钥: 1. 在 Workbench **Webhook** 选项卡中,点击您想要滚动私钥的每个端点。 1. 导航到溢出菜单 (⋯),然后点击**滚动私钥**。您可以选择立即使当前私钥过期,也可以延迟 24 小时后再让它过期,以便让自己有时间更新服务器上的验证码。这段时间内,端点会有多个密钥处于活动状态。Stripe 会为每个私钥生成一个签名,直到过期。 ### 验证事件是否是从 Stripe 发送的 如果未验证,攻击者可能会向您的端点发送伪造的 Webhook 事件,从而触发诸如履行订单、授予账户访问权限或修改记录等操作。在根据 Webhook 事件采取行动之前,请务必验证这些事件是否源自 Stripe。 请同时采用以下两种保护措施: - **IP 地址白名单**:Stripe 会从一组固定的 [IP 地址](https://docs.stripe.com/ips.md) 发送 Webhook 事件。请配置您的服务器或防火墙,使其仅接受来自这些地址的 Webhook 请求。 - **签名验证**:Stripe 会在每个 Webhook 事件的 `Stripe-Signature` 报头中添加签名。使用我们的[官方库](https://docs.stripe.com/webhooks.md#verify-official-libraries)或[手动方式](https://docs.stripe.com/webhooks.md#verify-manually)验证此签名,以确认该事件并非由第三方发送或修改。 以下部分描述了 Webhook 签名的验证方式: 1. 检索您的端点的密钥。 1. 验证签名。 #### 检索您的端点的密钥 使用 Workbench 并转到 **Webhooks** 标签页查看所有端点。选择要获取密钥的端点,然后单击**单击显示**。 Stripe 为每个端点生成一个唯一私钥。如果您为[测试和真实 API 私钥](https://docs.stripe.com/keys.md#test-live-modes)使用相同的端点,则每个端点的私钥各不相同。此外,如果您使用多个端点,则必须要为您想用来验证签名的每个端点都获取一个私钥。有了这种设置,Stripe 便可开始对它发送到端点的每个 Webhook 进行签名。 ### 防止重放攻击 [重放攻击 (Replay Attack)](https://en.wikipedia.org/wiki/Replay_attack)是攻击者截获有效的有效载荷及其签名,然后再重新传输。为了减轻这类攻击,Stripe 在 `Stripe-Signature` 头内包含了时间戳。因为此时间戳是已签名的有效载荷的一部分,因此它也由签名验证,所以攻击者在不使签名无效的情况下不能更改时间戳。如果签名有效,但时间戳太旧,则您可以让您的应用程序拒绝有效载荷。 我们的库在时间戳和当前时间之间有 5 分钟的默认容差。您可以通过在验证签名时提供附加参数来更改此容差。使用网络时间协议([Network Time Protocol,简称NTP](https://en.wikipedia.org/wiki/Network_Time_Protocol)),确保您的服务器时钟的准确性,并与 Stripe 的服务器时间同步。 > 请勿使用容差值 `0`。使用容差值为 `0` 将完全禁用近期检查。 每当向您的端点发送事件时,Stripe 都会生成时间戳和签名。如果 Stripe 重试一个事件(例如,您的端点之前回复了一个非 `2xx` 的状态码),则我们会为新的发送尝试生成新的签名和时间戳。 ### 快速返回 2xx 响应 您的[端点](https://docs.stripe.com/webhooks.md#example-endpoint)必须要快速返回一个成功的状态码 (`2xx`),然后才能执行任何可能导致超时的复杂逻辑。例如,您必须返回一个 `200` 响应,然后才能在您的会计系统中将客户的账单更新为已支付。 ## See also - [将事件发送到 Amazon EventBridge](https://docs.stripe.com/event-destinations/eventbridge.md) - [将事件发送到 Azure Event Grid](https://docs.stripe.com/event-destinations/eventgrid.md) - [精简事件类型列表](https://docs.stripe.com/api/v2/core/events/event-types.md) - [快照事件类型列表](https://docs.stripe.com/api/events/.md) - [交互式 Webhook 端点构建器](https://docs.stripe.com/webhooks/quickstart.md)