言零的博客

Stripe 协议支付自动化深度拆解:从 HAR 抓包到纯 API 全链路实现

2026 年 08 月 16 日 22:00

免责声明:本文仅供安全研究与技术学习。文中描述的技术手段仅用于分析 Stripe 支付协议的工作原理,帮助支付系统开发者理解安全边界。请勿将相关技术用于任何未授权的操作。


前言

Stripe 是全球最主流的支付处理商,几乎所有主流 AI 平台(对话类、图像类)都用它处理信用卡支付。

但很少有人深入研究 Stripe Checkout 背后的协议细节。多数认知停留在"调一下 API、跳个 Checkout 页面、扣完款就行"的层面。

实际上 Stripe 的 Checkout Session 协议远比想象的复杂:从 checkout_ui_modecustomhosted 两种模式,到 ConfirmationToken 的创建机制,再到 PaymentIntentrequires_action 状态下如何处理 3DS 验证挑战,每一步都牵涉大量协议细节、反欺诈检测和时序要求。

本文从 HAR 抓包出发,逐层拆解 Stripe 协议支付的完整链路,并附 5 套不同技术栈的实现源码。


零、方法论:如何逆向 Stripe 支付协议

所有协议支付的起点都是 HAR 抓包。

0.1 什么是 HAR 文件

HAR(HTTP Archive)是浏览器开发者工具导出的完整 HTTP 交互记录。包含每个请求的 URL、Headers、Body、Response,以及精确的时序信息。

打开 Chrome DevTools → Network → 完成一次真实支付 → 右键 → Save all as HAR with content。

这个 HAR 文件就是 ground truth。所有协议支付的字段、值、顺序、编码方式,都以 HAR 为准。代码实现、第三方文档、安全审计报告都可能过时或有误,只有 HAR 是当时浏览器真实发出的请求。

0.2 HAR 分析流程

  1. 筛选关键域名:api.stripe.comm.stripe.comcheckout.stripe.com、平台 API 域名
  2. 按时间排序:找出完整的请求链路
  3. 对比成功和失败:同一流程的成功 HAR 和失败 HAR,diff 出关键差异
  4. 提取稳定字段:跨多个 HAR 都一致的值可以硬编码
  5. 标记动态字段:每次不同的值(Session ID、Token、时间戳)需要运行时获取

0.3 经过验证的 HAR 请求链路

通过分析多个成功支付的 HAR 文件,提取出标准的浏览器 Checkout 请求序列:

1. POST /api/v1/payments/checkout → 创建 session (oaics_xxx)
2. GET stripe/v1/elements/sessions → 获取 Elements 配置 (deferred_intent)
3. POST /api/v1/payments/checkout/taxes → 税务重算 (US免税地址可消除VAT)
4. GET stripe/v1/elements/sessions → 更新后的 amount
5. POST stripe/v1/confirmation_tokens → 卡号 → ctoken_xxx
6. POST /api/v1/payments/checkout/confirm → 平台确认 → pi_xxx client_secret
7. POST stripe/v1/payment_intents/{pi}/confirm → Stripe 侧扣款
8. GET /checkout/verify → redirect_status=succeeded

每一步都有大量细节。下面逐一拆解。


一、为什么要做"协议支付"

协议支付(Protocol Payment)的定义:用纯 HTTP API 请求模拟浏览器的完整操作链路,包括 Stripe 的内部 API(非官方 SDK 暴露的那些),直接在协议层完成支付,无需浏览器。

对比浏览器支付(用户打开 Checkout 页面 → 填卡号 → 点支付 → 完成),协议支付的优势在于:

  1. 自动化:批量管理订阅时手动操作不现实
  2. 性能:浏览器(Puppeteer/Playwright)启动慢、占内存大、不稳定;协议支付 <5s 完成
  3. 环境隔离:服务端运行,不需要 GUI 环境
  4. 学习价值:深入理解支付系统的工作原理

二、Stripe Checkout Session 的两种模式

Stripe Checkout 有两种 checkout_ui_mode,这是整个协议支付的第一个分叉点:

2.1 Custom 模式 (oaics_ 前缀)

POST /payments/checkout
{
 "checkout_ui_mode": "custom",
 "currency": "PHP",
 "plan_type": "plus"
}
→ {"checkout_session_id": "oaics_xxx", "stripe_publishable_key": "pk_live_..."}

Custom 模式返回的 Session ID 以 oaics_ 开头。这种模式下:

2.2 Hosted 模式 (cs_live_ 前缀)

POST /payments/checkout
{
 "checkout_ui_mode": "hosted",
 "currency": "PHP",
 "plan_type": "plus"
}
→ {"checkout_session_id": "cs_live_xxx", "url": "https://checkout.stripe.com/..."}

Hosted 模式返回 cs_live_ 前缀的 Session。这种模式下:

两种模式的选择取决于平台的 billing 配置和你的需求。一般来说:

2.3 决定性参数:checkout_ui_mode

通过大量测试验证,决定返回 oaics_ 还是 cs_live_ 的唯一参数就是 checkout_ui_mode

// 返回 oaics_ 的请求
{
 "entry_point": "all_plans_pricing_modal",
 "plan_name": "basic_plan",
 "billing_details": {"country": "PH", "currency": "PHP"},
 "checkout_ui_mode": "custom"
}

// 返回 cs_live_ 的请求
{
 "plan_name": "basic_plan",
 "billing_details": {"country": "PH", "currency": "PHP"},
 "checkout_ui_mode": "hosted",
 "cancel_url": "https://platform.example.com/#pricing",
 "locale": "zh-CN"
}

注意差异:

Hosted 模式的响应多一个 url 字段——这是 Stripe 托管页面的完整 URL,可以直接在浏览器中打开。

2.4 _stripe_version 的版本差异

这是一个极其容易忽略的细节。两种模式使用不同的 _stripe_version

模式 _stripe_version
Custom (oaics_) 2025-03-31.basil
Hosted (cs_live_) 2025-03-31.basil; checkout_server_update_beta=v1; checkout_manual_approval_preview=v1

Hosted 模式的版本串后面带了两个 beta flag。如果在 cs_live_ 请求中用了 Custom 的短版本串,Stripe 会返回字段缺失或行为不一致。

2.5 两种模式的完整字段对比

字段 Custom (oaics_) Hosted (cs_live_)
确认端点 payment_intents/{pi}/confirm payment_pages/{cs_live}/confirm
顶层 source elements checkout
顶层 version 2021 custom
selection_flow merchant_specified automatic
elements_session_client 有(8 个子字段)
elements_options_client 有(2 个子字段)
link_brand link
checkout_config_id 有(两层不同值)
hCaptcha 位置 pmd[radar_options][hcaptcha_token] passive_captcha_token(顶层)
init_checksum
js_checksum
rv_timestamp
expected_amount

这两套字段集完全不能混用。这也是很多人协议支付做不通的原因——用了 oaics_ 的字段去调 cs_live_ 的接口,反过来也一样。


三、Custom 模式协议流程详解

Custom 模式是最干净的协议支付路径。完整流程 8 步:

┌──────────────────────────────────────┐
│ 1. 刷新 Session(获取最新 Token) │
│ 2. 查询账号信息(确认当前订阅状态) │
│ 3. 获取定价信息(币种+金额) │
│ 4. 创建 Checkout Session │
│ 5. 更新税务信息(Billing 地址) │
│ 6. 创建 ConfirmationToken(卡号→Token)│
│ 7. 确认支付(Confirm Checkout) │
│ 8. 确认 PaymentIntent(Stripe 侧) │
└──────────────────────────────────────┘

3.1 创建 ConfirmationToken

这是协议支付最核心的一步。浏览器里,Stripe.js 会在 iframe 中收集卡号,然后调用 Stripe 的内部 API 创建一个 ConfirmationTokenctoken_ 前缀)。

协议支付需要直接模拟这个请求:

POST https://api.stripe.com/v1/confirmation_tokens
Content-Type: application/x-www-form-urlencoded

type=card
&payment_method_data[type]=card
&payment_method_data[card][number]=4242424242424242
&payment_method_data[card][exp_month]=12
&payment_method_data[card][exp_year]=2029
&payment_method_data[card][cvc]=123
&payment_method_data[billing_details][name]=John Doe
&payment_method_data[billing_details][address][country]=US
&payment_method_data[billing_details][address][postal_code]=97201
&key=pk_live_xxx
&_stripe_version=2025-03-31.basil

几个关键细节:

  1. key 必须是平台的 Stripe Publishable Key,不是你自己的
  2. _stripe_version 必须匹配当前 Stripe.js 的 API 版本
  3. 需要传 Stripe.js 的指纹参数:guidmuidsidtime_on_page
  4. Referer 必须是平台域名,Stripe 会校验

卡号格式细节

HAR 验证发现,浏览器发送的卡号带空格4004 1641 0185 9397),而不是纯数字。不过 Stripe 接受两种格式。

guid/muid/sid 格式

这三个指纹 ID 的格式比很多人以为的更复杂。HAR 验证的正确格式是 42 字符

guid = randomUUID() + randomHex(6)
 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx + 6位hex
 = 36字符UUID + 6字符 = 42字符

例如:a1b2c3d4-e5f6-4789-abcd-ef0123456789abc123

有些实现用 32 位纯 hex 也能工作,但 HAR 中浏览器发送的是 42 字符格式。

Attribution 元数据(两层结构)

confirmation_tokens 请求中有一组 client_attribution_metadata 字段,这些是 Stripe 用来追踪支付集成来源的元数据:

payment_user_agent = stripe.js/...
client_session_id =  # 每 session 唯一
merchant_integration_source = elements # ← oaics_ 固定 "elements"
merchant_integration_version = 2021
merchant_integration_subtype = payment-element
payment_intent_creation_flow = deferred
payment_method_selection_flow = merchant_specified # ← oaics_ 固定
additional_elements = [expressCheckout, payment, address]

这些字段在 confirmation_tokens 请求中出现两次——一次在 payment_method_data 内部,一次在顶层。两层的值大部分相同,但 merchant_integration_source 在 confirm 请求中变成 l1。这个细节从 HAR 中才能发现。

请求头

HAR 中 Stripe API 请求的完整请求头:

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Origin: https://js.stripe.com
Referer: https://js.stripe.com/
Sec-CH-UA: "Not=A?Brand";v="99", "Chromium";v="151"
Sec-CH-UA-Mobile: ?0
Sec-CH-UA-Platform: "Windows"
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Accept-Language: en-US,en;q=0.9

注意 Accept-Language 必须存在。这是纯协议支付跑通的关键 bug 之一——缺少这个 header 会导致 Stripe 返回非预期的响应格式。

返回值:

{
 "id": "ctoken_1xxx",
 "type": "card",
 "payment_method_preview": {
 "card": {
 "brand": "visa",
 "last4": "4242"
 }
 }
}

3.2 确认 Checkout

拿到 ConfirmationToken 后,调用平台的 confirm 接口:

POST /api/v1/payments/checkout/confirm
{
 "checkout_session_id": "oaics_xxx",
 "confirmation_token": "ctoken_1xxx"
}

这一步平台会拿着你的 token 去 Stripe 创建 PaymentIntent 并尝试扣款。

如果一切顺利(卡余额充足、无 3DS 要求),PaymentIntent 直接变成 succeeded,订阅激活。

如果遇到 3DS 验证,流程会更复杂——我们在第五节讲。

3.3 Elements Session — 获取支付配置

在创建 ConfirmationToken 之前,需要先调用 Stripe 的 elements/sessions API 获取支付配置信息。这一步在 HAR 中被调用两次——一次在税务更新前,一次在税务更新后。

GET https://api.stripe.com/v1/elements/sessions?deferred_intent[mode]=subscription
 &deferred_intent[amount]=98214
 &deferred_intent[currency]=php
 &deferred_intent[setup_future_usage]=off_session
 &deferred_intent[payment_method_types][0]=card
 &type=deferred_intent
 &key=pk_live_xxx
 &_stripe_version=2025-03-31.basil

关键返回值:

3.4 税务地址优化 — US 免税地址消除 VAT

这是一个实际操作中极其重要的技巧。

很多 SaaS 平台默认按账号注册地收税。比如菲律宾区的订阅,标价 ₱982.14 但实际会加 12% VAT 变成 ₱1,100+。

解决方案:在 taxes API 调用中使用美国免税州地址。

POST /api/v1/payments/checkout/taxes
{
 "checkout_session_id": "oaics_xxx",
 "billing_details": {
 "name": "John Doe",
 "address": {
 "country": "US",
 "state": "OR",
 "city": "Portland",
 "postal_code": "97201",
 "line1": "123 Main St"
 }
 }
}

美国有 5 个无销售税的州:

HAR 验证:使用 US 地址后 tax=0,最终金额保持 ₱982.14。

重要taxes 和后续 confirmation_tokens 中的地址必须一致。同一个 session 内选择一个地址后全程复用。

3.5 PaymentIntent 确认 — Stripe 侧扣款

平台的 /confirm 返回 client_secret(格式:pi_xxx_secret_xxx)后,需要调用 Stripe 的 payment_intents/confirm 完成实际扣款:

POST https://api.stripe.com/v1/payment_intents/{pi_id}/confirm

return_url = https://platform.example.com/checkout/verify?stripe_session_id=oaics_xxx
confirmation_token = ctoken_xxx
key = pk_live_xxx
_stripe_version = 2025-03-31.basil
client_secret = pi_xxx_secret_xxx
client_attribution_metadata[client_session_id] = 
client_attribution_metadata[merchant_integration_source] = l1

注意这里 merchant_integration_source 变成了 l1,不再是 elements

PI 拒卡重试

HAR 验证了一个重要特性:同一个 PaymentIntent 被拒卡后可以用新的 ConfirmationToken 重试。

流程:

  1. 第一张卡 → generic_decline → PI 状态 requires_payment_method
  2. 创建新的 confirmation_token(用另一张卡)
  3. 再次调用 payment_intents/confirm
  4. 第二张卡 → succeeded

这意味着拒卡不需要重建整个 Checkout Session,只需要换卡重建 token。

协议支付需要严格的 Cookie 隔离。一个常见的 bug 是平台的 Cookie 被发送到 Stripe API,或反过来。

最佳实践:使用两个独立的 HTTP 客户端:

# 平台客户端 — 带 Cookie(session token)
platform_http = HttpClient(cookies_enabled=True)
platform_http.set_cookie("__Secure-platform-auth", session_token)

# Stripe 客户端 — 不带 Cookie
stripe_http = HttpClient(cookies_enabled=False)

绝不让 Stripe 客户端发送平台 Cookie,也不要让平台客户端发送 Stripe 的 Cookie。


四、Hosted 模式协议流程详解

Hosted 模式(cs_live_)的流程更复杂,因为需要模拟 Stripe Payment Page 的完整内部协议。

┌────────────────────────────────────────────────┐
│ 1. 创建 Checkout (hosted) → cs_live_ session │
│ 2. 更新税务 │
│ 3. 分配信用卡 │
│ 4. 初始化 Payment Page → ppage_ token │
│ 5. 获取 Elements Session │
│ 6. 创建 ConfirmationToken │
│ 7. 确认 Checkout (内部 confirm) │
│ 8. 平台 Approve 操作 │
│ 9. 轮询 PaymentIntent 状态 │
│ 10. 处理 3DS Challenge(如果需要) │
│ 11. 验证最终状态 │
└────────────────────────────────────────────────┘

4.1 Payment Page 初始化

GET https://checkout.stripe.com/api/payment-page/{cs_live_xxx}/init
→ {"ppage_token": "ppage_xxx", "merchant_name": "..."}

这是 Hosted 模式独有的。返回一个 ppage_ token,后续所有 Stripe Payment Page API 都需要这个 token。

4.2 Elements Session(Hosted 模式特殊字段)

Hosted 模式的 elements/sessions 请求比 Custom 模式多出很多字段:

POST https://api.stripe.com/v1/elements/sessions

deferred_intent[mode] = subscription
deferred_intent[amount] = 98214
deferred_intent[currency] = php
deferred_intent[setup_future_usage] = off_session
deferred_intent[payment_method_types][0] = card
deferred_intent[payment_method_configuration][id] = pmc_xxx
currency = php
elements_init_source = custom_checkout
referrer_host = platform.example.com
stripe_js_id = 
locale = zh
type = deferred_intent
checkout_session_id = cs_live_xxx
_stripe_version = 2025-03-31.basil; checkout_server_update_beta=v1; ...

关键差异:

返回值中有一个重要字段 passive_captcha.rqdata——这是 hCaptcha Enterprise 的 rqdata,后续 confirm 如果触发 challenge 需要用到。

4.3 Hosted 模式的 Confirm 请求

cs_live_ 的 confirm 走的是 payment_pages/confirm,不是 payment_intents/confirm

POST https://api.stripe.com/v1/payment_pages/{cs_live_xxx}/confirm

这个请求的 body 字段比 Custom 模式多很多:

elements_session_client[client_betas][0] = custom_checkout_server_updates_1
elements_session_client[client_betas][1] = custom_checkout_manual_approval_1
elements_session_client[elements_init_source] = custom_checkout
elements_session_client[referrer_host] = platform.example.com
elements_session_client[session_id] = elements_session_xxx
elements_session_client[stripe_js_id] = 
elements_session_client[locale] = zh
elements_session_client[is_aggregation_expected] = false
elements_options_client[saved_payment_method][enable_save] = auto
elements_options_client[saved_payment_method][enable_redisplay] = auto
init_checksum = 
js_checksum = 
rv_timestamp = 
expected_amount = 98214
expected_payment_method_type = card
version = 

js_checksumrv_timestamp 是从 Stripe.js 部署状态动态提取的,init_checksum 来自 init 响应。

4.4 Approve 机制与 Sentinel Token

Hosted 模式下,payment_pages/confirm 成功后,状态不会直接变成 succeeded,而是进入 requires_approval(HAR 中 confirm 返回 status=open)。这时需要调用平台的 approve 接口:

POST /api/v1/payments/checkout/approve
Authorization: Bearer {access_token}
X-Vendor-Challenge-Token: {sentinel_token}

Sentinel Token 结构

这里的 challenge_token 是一个编码后的 JSON 对象,内部包含多个通过算力证明(PoW)和 Turnstile 验证生成的字段:

{
 "p": "gAAAAAB...", // 主负载(加密后的 PoW + session 信息)
 "c": "...", // enforcement_token(基于 FNV-1a 的算力证明结果)
 "id": "", // 一次性标识
 "flow": "platform_checkout", // 标记来源流程
 "t": "" // 4000+ 字符的 Turnstile 验证 token
}

t 字段是核心——如果为空字符串,approve 立即返回 blocked。它来自 Cloudflare Turnstile 的隐式挑战验证(不是肉眼可见的验证码)。

Sentinel Token 生成流程

完整的 Sentinel 生成需要两个组件协作:

1. 生成 requirements_token(本地 seed + difficulty "0")
2. POST sentinel.platform.com/backend-api/sentinel/req
 → 返回 {turnstile: {dx: "..."}, ...}
3. 用 server seed 生成 enforcement_token(FNV-1a 算力证明)
4. 在 Node.js VM 中执行 Turnstile SDK(需要 jsdom 模拟 DOM 环境)
 → 返回 turnstile token(~4388 字符)
5. 组装 JSON → Base64 → 设为请求头

关键点:

Approve 的正确时机

关键发现:Approve 不应该主动调用。正确的流程是 fall-through:

confirm → status=open, requires_approval
 ↓
 等待 Stripe 回调
 ↓
 平台自动完成 approve(不需要客户端调)
 ↓
 poll payment_page → PI 出现
 ↓
 可能 requires_action → verify_challenge
 ↓
 poll 到 succeeded → 完成

但某些边缘情况下(平台回调未触发),客户端需要手动 approve。此时 Sentinel token 是必须的。


五、3DS 验证挑战

当 Stripe 判断交易需要额外验证时,PaymentIntent 会进入 requires_action 状态,附带 3DS challenge 信息。

5.1 Challenge 类型

Stripe 使用 hCaptcha Enterprise 作为 3DS 验证手段:

{
 "status": "requires_action",
 "next_action": {
 "type": "verify_with_challenge",
 "verify_with_challenge": {
 "url": "https://challenges.stripe.com/...",
 "challenge_type": "hcaptcha"
 }
 }
}

5.2 verify_challenge 完整请求格式

HAR 中验证的 verify_challenge 完整请求:

POST https://api.stripe.com/v1/payment_intents/{pi_id}/verify_challenge

captcha_response = {hcaptcha_token} # P0开头的长字符串
captcha_vendor_name = hcaptcha # 固定值
captcha_sitekey = xxxxxxxx-xxxx-xxxx # Stripe 的 hCaptcha sitekey
expected_amount = 98214 # 交易金额
expected_currency = php # 币种
key = pk_live_xxx # Publishable Key
_stripe_version = 2025-03-31.basil; checkout_server_update_beta=v1; ...
client_secret = pi_xxx_secret_xxx # PI 的 client_secret

注意:

5.3 hCaptcha Enterprise 的特殊性

Stripe 使用的不是普通 hCaptcha,而是 hCaptcha Enterprise。区别:

  1. rqdata 参数:普通 hCaptcha 不需要 rqdata,Enterprise 必须传
  2. sitekey 是 Stripe 专有的:从 elements/sessions 响应的 passive_captcha 字段中提取
  3. invisible 模式:浏览器里看不到验证码图片,但后台在做风控评估
# 从 elements_session 响应中提取
rqdata = elements_session["passive_captcha"]["rqdata"]
sitekey = elements_session["passive_captcha"]["sitekey"]

5.4 时效性问题

hCaptcha token 有时效性——浏览器里 ~4 秒解出即有效,但远程打码服务可能需要 30-120 秒。如果超时:

解决方案分级:

  1. 最优:本地 headless 浏览器解 hCaptcha(<5s,成功率 95%+)
  2. 次优:支持 Enterprise rqdata 的快速打码 API(如 YesCaptcha,~15s)
  3. 兜底:如果 PI 已经进入 requires_action,可以循环重试 verify_challenge(每次用新 captcha token)

5.5 3DS vs hCaptcha 的区别

重要澄清:Stripe 的 verify_with_challenge 并非传统银行 3DS 验证(3D Secure)。它是 Stripe 自有的风控挑战,使用 hCaptcha 作为人机验证手段。

真正的银行 3DS(如 Visa Secure)会打开银行页面要求输入 OTP/密码,这个在纯协议支付中无法绕过——但使用虚拟信用卡(VCC)通常不会触发银行 3DS,只会触发 Stripe 自身的 hCaptcha challenge。


六、TLS 指纹对抗

纯 HTTP 请求最大的挑战不是协议逻辑,而是 TLS 指纹

Stripe 和 Cloudflare 都会检查 TLS Client Hello 的指纹(JA3/JA4),如果发现你的 TLS 握手特征不像浏览器,直接返回 403/429。

6.1 指纹伪装

Python 项目使用 curl_cffi 库,支持模拟多种浏览器的 TLS 指纹:

PROFILES = [
 ("chrome131", "Mozilla/5.0 ... Chrome/131.0"),
 ("chrome124", "Mozilla/5.0 ... Chrome/124.0"),
 ("safari18", "Mozilla/5.0 ... Safari/605.1"),
 ("firefox117", "Mozilla/5.0 ... Firefox/117.0"),
]

Node.js 项目使用 CycleTLS

import initCycleTLS from 'cycletls';
const cycleTLS = await initCycleTLS();
const response = await cycleTLS(url, {
 ja3: '771,4865-4866-4867...',
 userAgent: 'Mozilla/5.0 ...'
});

6.2 指纹轮换策略

当遇到 403/429 时,自动切换到下一个 TLS 指纹配置:

class TLSFingerprintManager:
 def rotate_on_failure(self):
 self.exclude_current()
 return self.pick_next()

最多重试 3 次,每次使用不同的浏览器指纹。

6.3 TLS 指纹与 User-Agent 一致性

一个常见错误是 TLS 指纹和 User-Agent 不匹配。比如用 Chrome 131 的 TLS 指纹配置但 User-Agent 写的是 Chrome 124,Cloudflare 的 bot detection 会立即发现这种不一致。

正确做法:TLS profile 和 UA 必须成对配置:

PROFILES = [
 {
 "tls": "chrome131",
 "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/131.0.0.0 Safari/537.36",
 "sec_ch_ua": '"Not=A?Brand";v="99", "Chromium";v="131"'
 },
 {
 "tls": "safari18",
 "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) ... Safari/605.1.15",
 "sec_ch_ua": None # Safari 不发 sec-ch-ua
 }
]

6.4 HTTP 协议版本

另一个细节:Stripe 的 API 请求在浏览器中走 HTTP/2,但 payment_intents/confirm 在某些实现中用 HTTP/1.1 反而更稳定。

HAR 中观察到:

使用 curl_cffi 时默认走 HTTP/2(如果服务端支持),CycleTLS 需要在选项中显式指定。


七、Stripe.js 指纹参数

Stripe 的 confirmation_tokenspayment_intents/confirm 接口都需要一组指纹参数。这些参数由 Stripe.js 在前端收集,协议支付需要正确模拟:

参数 含义 生成方式
guid 全局唯一 ID UUID + 6位 hex = 42 字符(见 3.1 节)
muid 设备 ID 同 guid 格式,42 字符
sid 会话 ID 同 guid 格式,42 字符
time_on_page 页面停留时间 随机 8-30 秒(模拟真实用户)
key Publishable Key 从 Checkout Session 获取
_stripe_version API 版本 从 Stripe.js 的 basil 部署中提取

7.1 m.stripe.com/6 — 指纹注册

在所有业务请求之前,Stripe.js 会向 m.stripe.com/6 发送 4 次 POST 请求注册设备指纹。HAR 中的完整调用链:

POST https://m.stripe.com/6 (×4)

每次请求的 body 是 application/x-www-form-urlencoded,包含:

v2 = 1
id =  # 与 confirmation_tokens 中的 guid 不同
tag = 
src = js
a = 

tag 的值依次为:

  1. stripejs-init-started
  2. stripejs-init-complete
  3. adyen-component-loaded(如果有 Adyen)
  4. payment-element-mounted

a 字段包含浏览器环境的详细指纹数据(屏幕尺寸、时区、语言、plugin 列表等)。

实际影响:不发这些请求也能支付成功,但长期使用同一个 guid/muid 而从未注册过指纹可能触发 Stripe Radar 的异常检测。建议至少发一次 stripejs-init-complete

7.2 r.stripe.com/b — 遥测上报

Stripe.js 还会持续向 r.stripe.com/b 发送遥测数据:

POST https://r.stripe.com/b
Content-Type: text/plain

{JSON payload}

这个请求的 payload 是 JSON(不是 form-urlencoded),包含支付流程中的各种事件指标(加载时间、渲染时间、错误数)。

{
 "type": "measurement",
 "event_name": "paymentElement.mount",
 "metrics": {"duration": 342},
 "client_session_id": "",
 "publish_key": "pk_live_xxx"
}

实际影响:纯可选。不发不影响支付成功。但如果你的实现在生产中运行,偶尔发几次遥测可以让你的流量看起来更像真实浏览器。

7.3 动态提取 Stripe.js 版本

Stripe.js 的版本(_stripe_version)和构建哈希(rv/sv)不是固定的,需要从 Stripe 的部署状态接口动态提取:

// 获取当前 Stripe.js 部署信息
const status = await fetch('https://js.stripe.com/deploy_status_henson.json');
const { basil } = await status.json();
// basil.rv = "2025-03-31" → _stripe_version = "2025-03-31.basil"

Custom 模式只需要基础版本:2025-03-31.basil

Hosted 模式需要追加 beta flags:

2025-03-31.basil; checkout_server_update_beta=v1; custom_checkout_beta=v1; ...

这些 beta flags 从 deploy_status_henson.json 的 metadata 或 checkout 页面的初始化脚本中提取。Stripe 大约每 1-2 周更新一次部署版本。

7.4 js_checksum 和 rv_timestamp

Hosted 模式的 payment_pages/confirm 需要两个动态校验值:

这两个值随 Stripe.js 版本更新而变化。如果使用过期的 checksum/timestamp,confirm 可能会被静默拒绝。

deploy = requests.get("https://js.stripe.com/deploy_status_henson.json").json()
js_checksum = deploy["basil"]["js_checksum"]
rv_timestamp = deploy["basil"]["rv_timestamp"]

八、代理池架构

协议支付的另一个核心组件是代理池。Stripe 和平台都会检查请求的源 IP,用数据中心 IP 直连容易被标记。

8.1 双代理池

最佳实践是使用两个独立的代理池:

8.2 代理健康检查

class ProxyVerifier:
 def verify(self, proxy_url: str, expected_country: str):
 resp = requests.get("https://ipapi.co/json/", proxies=proxy_url)
 actual = resp.json()["country_code"]
 if actual != expected_country:
 raise ProxyCountryMismatchError(expected_country, actual)

8.3 出口 IP 一致性

关键规则:创建 Checkout Session 和确认支付时,必须使用同一个出口 IP。Stripe 会比对两次请求的 IP,如果不一致会拒绝交易。

这意味着整个支付流程中的所有请求(从创建 session 到 confirm)必须绑定同一个代理出口。如果代理池做了负载均衡,需要在支付流程中"钉住"一个代理节点。

8.4 代理类型选择

代理类型 适用场景 风险
住宅代理(Residential) Stripe API / 平台 API 最安全,IP 信誉高
ISP 代理(Static Residential) 长期固定使用 中等,某些已被标记
数据中心代理(Datacenter) 测试/开发 高风险,Stripe Radar 会标记
移动代理(Mobile) 高价值交易 最安全但最贵

生产环境建议住宅代理。数据中心 IP 在 Stripe 的风控系统中通常有较高的 risk score。

8.5 代理故障降级

代理不可用时的处理策略:

async def get_proxy_with_fallback(pool, country):
 proxy = await pool.acquire(country=country)
 if proxy:
 return proxy
 # 降级:尝试其他国家的代理
 proxy = await pool.acquire(country="US")
 if proxy:
 return proxy
 # 最终降级:直连(仅限测试环境)
 if config.allow_direct:
 return None
 raise NoProxyAvailableError()

注意:不要在生产环境用直连降级——数据中心 IP 直连 Stripe 几乎必被拒。


九、状态机设计

完整的协议支付需要处理多种失败场景和重试逻辑。最佳实践是使用状态机:

初始化 → 刷新Session → 检查权限 → 获取定价 → 创建Checkout
 ↓ ↓ ↓ ↓ ↓
 失败→退出 失败→退出 已有订阅→跳过 失败→退出 失败→退出
 ↓
 更新税务 → 创建Token → 确认支付
 ↓ ↓ ↓
 失败→退出 卡被拒→换卡 3DS→验证
 ↓
 成功 / 失败

每个状态转移都需要处理:

9.1 Decline 错误分类

Stripe 的卡拒绝错误不是一个笼统的"失败",而是分为可重试和不可重试两类:

可重试(换卡后重试):

不可重试(放弃本次 session):

需要特殊处理

9.2 Fast-Path 优化

一个重要的状态机优化:如果 PI 已经 succeeded,跳过 hCaptcha。

HAR 中发现,有时 confirm 返回 requires_action(hCaptcha challenge),但 PI 实际上已经扣款成功。这时如果盲目去解 captcha,反而可能出错。

正确做法是在收到 requires_action 后,先查询 PI 状态:

pi = stripe.get(f"/v1/payment_intents/{pi_id}", key=pk_live)
if pi["status"] == "succeeded":
 # 已经扣款成功,跳过 captcha
 return PaymentResult.SUCCESS
elif pi["status"] == "requires_action":
 # 确实需要解 captcha
 solve_hcaptcha(pi)

9.3 Hosted 模式轮询

cs_live_ 模式在 confirm 之后需要轮询 payment_page 状态:

POST https://api.stripe.com/v1/payment_pages/{cs_live_xxx}/poll
→ {"payment_intent": "pi_xxx", "status": "open"} // 等待中
→ {"payment_intent": "pi_xxx", "payment_object_status": "requires_action"} // 需要 captcha
→ {"payment_intent": "pi_xxx", "payment_object_status": "succeeded"} // 完成

轮询间隔建议 2-3 秒,最多轮询 60 秒。如果超时未变化,检查是否需要手动 approve。


十、安全边界与最佳实践

10.1 Session 隔离

每次支付使用独立的 Session,不复用已失败的 Session。特别是 Hosted 模式下,approve 操作后的 Session 不可再次使用。

10.2 Dry-Run 模式

所有实现都应支持 Dry-Run 模式——执行完整流程但不提交最终的 confirm

if config.commit_payments:
 result = stripe.confirm_payment_intent(pi_id, token)
else:
 logger.info("Dry-run: skipping confirm")

这对于调试非常重要:你可以验证整个流程(创建 session、生成 token、税务计算)都正确,而不实际扣钱。

10.3 不要存储敏感数据

10.4 请求节奏模拟

纯协议支付的一个风险是请求速度太快。浏览器用户从打开支付页面到点击确认,通常需要 15-60 秒。如果你的自动化在 2 秒内完成全部请求,Stripe Radar 可能标记为异常。

建议在关键步骤间加入随机延迟:

await sleep(random.uniform(1.5, 3.0)) # 创建 session 后
await sleep(random.uniform(0.8, 2.0)) # 更新税务后
await sleep(random.uniform(1.0, 2.5)) # 创建 token 后
await sleep(random.uniform(0.5, 1.5)) # confirm 前

总时间控制在 8-30 秒之间比较自然。

10.5 并发控制

批量处理多个账号时,不要让所有请求同时发出:

建议使用信号量(semaphore)限制并发:

sem = asyncio.Semaphore(3) # 最多同时处理 3 个支付
async with sem:
 await process_payment(account)

十一、实现对比

文末提供的 5 套实现覆盖了不同的技术栈和使用场景:

实现 语言/框架 模式 特点
saas-pay-sdk Python / CLI Custom 最精简的 CLI 工具,TLS 指纹轮换,daemon 上报
subscription-manager Python Flask / Web UI Both 完整 Web 管理面板,支持绑卡、查账单、升级降级
payment-gateway Python Flask / Web UI Both 批量支付面板,账号池管理,barrier 模式批量确认
checkout-server Python / Local HTTP Both 本地支付编排服务器,Playwright 降级,SQLite 持久化
protocol-engine Node.js / Modules Both 模块化协议引擎,CycleTLS,动态 Stripe.js 版本抓取

每个实现都有自己的优缺点:


下载

以下是 5 套完整实现的源码下载链接:

项目 下载链接 说明
saas-pay-sdk 下载 Python CLI 工具
subscription-manager 下载 Flask Web 管理面板
payment-gateway 下载 批量支付网关面板
checkout-server 下载 本地支付编排服务器
protocol-engine 下载 Node.js 协议引擎模块

注意:所有源码中的 API Key、域名、账号信息均已替换为占位符。使用前需要根据你的实际环境配置 .env 文件。