Stripe Webhook签名验证失败:Raw Body、Endpoint Secret与时间戳排查
在 Webhook 专用路由上,让应用在任何 JSON body parser、字符集转换、日志脱敏或字段改写之前保留原始字节,并将该字节缓冲、原始 Stripe- 头和当前 endpoint 的 whsec_... signing secret 交给 Stripe 官方 SDK 验证。确保 CLI 转发产生的 secret、Workbench/
本文目录(18 节)
直接答案
在 Webhook 专用路由上,让应用在任何 JSON body parser、字符集转换、日志脱敏或字段改写之前保留原始字节,并将该字节缓冲、原始 Stripe- 头和当前 endpoint 的 whsec_... signing secret 交给 Stripe 官方 SDK 验证。确保 CLI 转发产生的 secret、Workbench/
一、先保存脱敏证据
为一次失败记录:接收时间、部署版本、路由、请求字节数、原始 body 的 SHA-256、签名头是否存在、签名中的时间戳与不可逆的 secret 指纹。不要记录完整 signing secret、完整签名头或包含客户数据的原始 body。
用这些证据区分“头丢失”、“body 被改写”、“secret 选错”和“时间戳不可接受”。不要为了调试而先关闭验签,因为这会让公网路由可以伪造支付事件。
二、Raw Body是原始字节而非重建JSON
JSON 对象中的空白、换行、字段顺序和转义表示可以改变,但解析后的业务值仍相同。签名校验不是对“等价 JSON 对象”验证,而是对 Stripe 发送时的负载字节和时间戳验证。
以下操作都可能破坏验签:先 JSON.parse 再 JSON.stringify,将 Buffer 按错误字符集转换,修改换行,执行 Unicode 归一化,对 body 做 trim,或由 API 网关重建 JSON。
三、Body Parser中间件的顺序
Express、Next.js、serverless runtime、Java/Spring、Python 框架和云函数对 body 的读取方式不同。全局 JSON parser 可能在 Webhook handler 之前已经消费 stream,后续无法取得真正 raw bytes。
查看框架当前版本和 Stripe 官方示例,对 Webhook 精确路由设置 raw body 读取,并确保它在通用 JSON parser 之前。不要为整个 API 禁用 JSON parser,只需对签名验证路由做明确分支。
四、检查Stripe-Signature头是否原样到达
反向代理、API Gateway、serverless adapter 或自定义边缘函数可能删除、合并或重命名请求头。检查应用层是否能读到 Stripe-,且没有只保留其中一段值。
不要手工挑选某个 v1 值并自己计算,官方 SDK 会按 Stripe 当前签名头格式和轮换策略处理。排查时只记录头是否存在、长度和脱敏结构,不记录完整值。
五、Endpoint Secret不是API Key
Webhook signing secret 通常以 whsec_ 类型标识,它不是 publishable key,也不是 secret API key。每个 Webhook endpoint 有自己的 signing secret,不能拿另一个 endpoint 的 secret 来验证。
在配置库中为 secret 存储不可逆指纹和 endpoint ID/环境标签,便于定位选错配置,但不输出真实 secret。部署时验证环境变量没有多余引号、换行、空格或未展开的占位符。
六、不要混用CLI与Dashboard/Workbench Secret
Stripe CLI 在本地转发事件时会提供用于该转发会话的 signing secret。这个 secret 与公网 Workbench/
为本地、staging 和 production 分别配置 endpoint 与 secret,不要在代码中根据失败后“试下一个 secret”。验签应根据请求到达的明确 host/route 选择唯一允许的配置集。
七、测试与生产环境分离
测试事件、测试 endpoint 与生产事件应在配置和数据处理上分离。不要因 URL 相同就假设 secret 相同,也不要用生产 secret 在开发机上调试。
记录 endpoint 配置来源、账户、模式和部署环境,但对 secret 只记指纹。事件验签后,还要按事件的 livemode/账户上下文执行业务边界检查,防止测试数据进入生产订单。
八、时钟与时间戳容差
签名验证使用时间戳来限制重放窗口。如果服务器时钟偏移过大,正确签名也可能因时间差失败。检查主机和容器时间,确保 NTP/时间同步健康。
不要通过无限增大容差或禁用时间戳检查来修复时钟问题,因为会扩大重放风险。先修正系统时钟,再使用 Stripe SDK 的安全默认或经审计的容差。
九、反向代理与API Gateway的Body改写
某些 API Gateway 会将 body 用 Base64 封装、转换字符集、解压或重建 JSON。serverless adapter 可能向处理器传入已解析对象,而非原始字节。只要 body 从 Stripe 到验签代码之间变化,验证就会失败。
在边缘与应用入口分别计算原始字节 SHA-256(仅在受控调试环境),如果哈希不同,逐层检查传输映射。不要用生产客户负载做全量日志对比。
十、密钥轮换与多签名窗口
在 signing secret 轮换期间,Stripe 可能在一段时间内使用多个活跃 secret/签名,以支持安全过渡。使用官方 SDK 和 Stripe 提供的轮换流程,不要自己拆签名头并只验证第一个值。
部署新 secret 时要先让所有实例具备正确验证能力,再结束旧 secret 的过渡。监控各版本的验签失败率和 secret 指纹,避免滚动部署中一半实例仍只认旧配置。
十一、先验签,再去重和处理
请求必须先验签成功,再信任 event ID、type 和 data object。验签后用 event ID 做幂等去重,将必需处理入持久队列或事务记录,然后快速返回成功 HTTP 响应。
不要在返回前执行长时间订单履约,否则 Stripe 可能因超时重试,产生合法的重复投递。签名验证不会自动提供业务幂等性,两者都必须实现。
十二、最小复现流程
- 创建一个仅做 raw body 读取、SDK 验签和 2xx 返回的最小端点。
- 用 Stripe CLI 向本地端点转发,只使用 CLI 当次提供的 secret。
- 在测试公网 endpoint 使用该 endpoint 自身 secret 复现,不复用 CLI secret。
- 逐项加回反向代理、serverless adapter、全局 parser、日志和业务中间件。
- 在每个边界对比 raw body 字节数和哈希,定位第一个改变字节的环节。
十三、常见错误
- 先解析 JSON,再将
JSON.stringify(event)交给验签函数。 - 把 Stripe secret API key 或 publishable key 当成 Webhook signing secret。
- 将 CLI 转发 secret 配到生产 endpoint,或反过来。
- 为排查方便把完整 secret、签名头和负载写入日志。
- 通过无限放大时间戳容差解决系统时钟偏移。
- 验签失败时继续解析并处理事件,导致伪造业务写入。
- 认为验签成功就不需要 event ID 去重和幂等处理。
十四、验收清单
- Webhook 路由在任何 JSON parser 之前获取原始字节。
Stripe-头原样可用,不被代理删除或错误拆分。Signature - 本地 CLI、staging 和 production 各使用自己 endpoint 的 secret。
- 应用配置的 secret 指纹与目标 endpoint 记录一致,日志不含 secret。
- 系统时钟同步,不需要不安全的宽容差。
- 密钥轮换期间新旧版本均能按官方流程验证。
- 验签失败不会触发任何订单、权益或退款写入。
- 验签成功后通过 event ID 去重,重复投递不会重复履约。
FAQ
1. 可以用解析后的JSON对象验签吗?
不可以。验签需要 Stripe 发送的原始请求字节。解析后再序列化可能改变空白、字段顺序和转义。
2. Endpoint secret和Stripe API secret key是一个吗?
不是。Webhook endpoint signing secret 用于验证该 endpoint 的事件签名,与用来调用 Stripe API 的 secret key 用途不同。
3. 本地CLI验证通过,生产为什么失败?
首先检查是否将 CLI 转发会话的 secret 误用到生产。生产必须使用公网 endpoint 自身的 signing secret,并确保代理未改写 body。
4. 签名验证通过后还需要去重吗?
需要。Stripe 可能合法重试事件,验签只证明请求来源和完整性,不保证每个 event ID 只投递一次。
5. 验签失败时能否先返回200避免重试?
不应将失败伪装成成功。应修复 raw body/secret/时钟问题,并返回与实际处理一致的响应;否则会丢失真实事件。
总结
Stripe Webhook 验签的核心是三个原样输入:原始 body 字节、原始 Stripe- 头和正确 endpoint 的 signing secret。从请求入口到 SDK 验证之间不允许任何 body 改写,并保持系统时钟可信。验签通过后再去重、入队和幂等履约,才是完整的支付事件处理链。
参考资料
- Stripe Docs: Resolve webhook signature verification errors
https:
- Stripe Docs: Receive Stripe events in your webhook endpoint
https:
- Stripe Docs: Process undelivered webhook events
https:
- Stripe Docs: Test a webhook endpoint