测试环境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
原因分析
- 请求体重复读取:调试时手动调用
verify_header已经读取了request.body的内容,此时请求体指针移到末尾。后续construct_event再次读取时会得到空字符串,导致签名验证失败。 - 请求内容格式错误:测试中用
params: event.to_json发送请求,Rails会自动将JSON字符串解析为表单参数,并设置Content-Type为application/x-www-form-urlencoded,和生成签名时的application/jsonpayload不一致。
修复方案
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
相关产品推荐
相关产品推荐

