You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

测试环境Stripe Webhook签名验证异常:RSpec测试中签名验证矛盾问题

问题:RSpec测试中Stripe Webhook签名验证异常

在RSpec执行请求测试时,调用Stripe::Webhook.construct_event会抛出Stripe::SignatureVerificationError异常。通过byebug调试时,Stripe::Webhook::Signature.verify_header返回true,但继续执行construct_event仍触发该异常。明明construct_event内部先调用verify_header,为什么调试时验证通过,实际调用却失败?


Webhook控制器代码

class WebHooks::StripeController < WebHooksController

  # Entry point for Stripe webhooks. This method
  # will verify the signature and dispatch to the
  # appropriate method. It will log warning if
  # the webhook type is unknown. The method dispatched is the
  # webhook type with underscores instead of dots.
  def create
    payload = request.body.read
    sig_header = request.headers['Stripe-Signature']
    event = nil
    byebug
    # Byebug Console
    Stripe::Webhook::Signature.verify_header(payload, sig_header, Rails.application.credentials.stripe[:signing_secret]) 
    # => True, this returns true

    begin
      event = Stripe::Webhook.construct_event(
        payload, sig_header, Rails.application.credentials.stripe[:signing_secret]
      )
    rescue JSON::ParserError => e
      # Invalid payload
      head :unprocessable_entity
      return
    rescue Stripe::SignatureVerificationError => e
      # Invalid signature
      Rails.logger.error("⚠️  Stripe signature verification failed.")
      head :unauthorized
      return
    end

    type = event.type.gsub('.', '_')

    begin
      public_send(type)
    rescue NoMethodError
      Rails.logger.warn("Unknown webhook type: #{params[:type]}")
      head :unprocessable_entity
    end
  end

end

测试代码

require 'rails_helper'

RSpec.describe "WebHooks::Stripe::Signature", type: :request do

  context "with a valid signature" do
    it "returns 200" do
      event = { type: "not_implemented" }
      headers = {
        "Stripe-Signature" => stripe_event_signature(event.to_json)
      }
      post "/web_hooks/stripe", params: event.to_json, headers: headers
      expect(response).to have_http_status(200) # This fails
    end
  end

  context "an invalid signature" do
    it "returns 401" do
      post "/web_hooks/stripe", params: { type: "not_implemented" }
      expect(response).to have_http_status(401)
    end
  end

end

Stripe辅助工具代码

module StripeTestHelper
  def stripe_event_signature(payload)
    time = Time.now
    secret = Rails.application.credentials.stripe[:signing_secret]
    signature = Stripe::Webhook::Signature.compute_signature(time, payload, secret)
    Stripe::Webhook::Signature.generate_header(
      time,
      signature,
      scheme: Stripe::Webhook::Signature::EXPECTED_SCHEME
    )
  end

end

原因分析

  1. 请求体重复读取:调试时手动调用verify_header已经读取了request.body的内容,此时请求体指针移到末尾。后续construct_event再次读取时会得到空字符串,导致签名验证失败。
  2. 请求内容格式错误:测试中用params: event.to_json发送请求,Rails会自动将JSON字符串解析为表单参数,并设置Content-Type为application/x-www-form-urlencoded,和生成签名时的application/json payload不一致。

修复方案

1. 控制器修复:复用请求体内容

读取一次请求体后复用,避免重复读取导致空内容:

class WebHooks::StripeController < WebHooksController
  def create
    # 仅读取一次请求体并复用
    payload = request.body.read
    # 重置请求体指针(可选,方便后续可能的操作)
    request.body.rewind
    sig_header = request.headers['Stripe-Signature']
    event = nil

    begin
      event = Stripe::Webhook.construct_event(
        payload, sig_header, Rails.application.credentials.stripe[:signing_secret]
      )
    rescue JSON::ParserError => e
      head :unprocessable_entity
      return
    rescue Stripe::SignatureVerificationError => e
      Rails.logger.error("⚠️  Stripe signature verification failed.")
      head :unauthorized
      return
    end

    type = event.type.gsub('.', '_')

    begin
      public_send(type)
    rescue NoMethodError
      Rails.logger.warn("Unknown webhook type: #{event.type}")
      head :unprocessable_entity
    end
  end
end

2. 测试代码修复:正确发送JSON请求

指定Content-Type为application/json,并将payload放在body参数中:

require 'rails_helper'

RSpec.describe "WebHooks::Stripe::Signature", type: :request do
  include StripeTestHelper

  context "with a valid signature" do
    it "returns 200" do
      event_payload = { type: "not_implemented" }.to_json
      headers = {
        "Stripe-Signature" => stripe_event_signature(event_payload),
        "Content-Type" => "application/json"
      }
      post "/web_hooks/stripe", body: event_payload, headers: headers
      expect(response).to have_http_status(200)
    end
  end

  context "an invalid signature" do
    it "returns 401" do
      post "/web_hooks/stripe", params: { type: "not_implemented" }
      expect(response).to have_http_status(401)
    end
  end
end

3. 辅助工具优化:固定时间避免时差问题

测试中固定时间,防止调试耗时超过Stripe签名的时间容忍范围:

module StripeTestHelper
  def stripe_event_signature(payload)
    # 固定时间戳,避免调试时差导致验证失败
    time = Time.now.to_i
    secret = Rails.application.credentials.stripe[:signing_secret]
    signature = Stripe::Webhook::Signature.compute_signature(time, payload, secret)
    Stripe::Webhook::Signature.generate_header(
      time,
      signature,
      scheme: Stripe::Webhook::Signature::EXPECTED_SCHEME
    )
  end
end

内容的提问来源于stack exchange,提问作者Romuloux

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 07:35:36