在线支付系列三Stripe 信用卡——一件跨境商品的卡支付之旅$149.99 的限量球鞋3 秒钟经历了什么纽约的 John 深夜刷手机在一个中国独立站上发现了一双限量球鞋标价 $149.99。他决定入手。页面弹出支付框他选择了信用卡支付输入了 Visa 卡号、有效期、CVC。突然屏幕中间弹出一个窗口——他的银行要求输入手机验证码。他打开短信输入 6 位数字点击确认。“Payment Successful.”这整个过程大约 10 秒。但在这 10 秒钟里John 的 $149.99 经历了一段横跨太平洋的旅程从他的 Chase 银行账户出发穿过 Visa 网络经过 Stripe 的收单系统最终到达中国独立站商户的 Stripe 余额里。中间那个弹窗是什么为什么 John 的卡号没有直接传到商户的服务器如果 John 过了三天说我没买过这双鞋怎么办这篇文章就从这笔 $149.99 说起。一、信用卡支付的特殊之处在上一篇中我们对接了支付宝和微信——它们的流程相对直接创建订单、生成二维码、收到回调、更新状态。信用卡支付的复杂度要高出一个量级因为它多了几层东西授权与捕获可以分离——钱可以先冻住过几天再真正扣3D Secure 身份验证——银行可能要求用户额外验证身份就是 John 收到的那个短信验证码弹窗PCI DSS 合规——你碰了信用卡号就要遵守一套严格的安全标准争议/拒付机制——持卡人可以在交易完成后反悔向银行申请退款好消息是Stripe 帮你处理了绝大部分复杂度。但你仍然需要理解这些概念才能做出正确的技术决策。二、授权与捕获先冻住再扣款让我们先理解信用卡支付中最核心的概念——授权Authorization和捕获Capture的分离。2.1 两步走的逻辑当 John 输入卡号点击支付时实际上发生了两件不同的事第一步授权Authorization 第二步捕获Capture ┌──────────────────────┐ ┌──────────────────────┐ │ Chase 银行冻结 $149.99 │ │ 实际从 John 卡里扣款 │ │ 返回授权码 │ ── 最多7天 → │ 资金进入清结算流程 │ │ 钱还没有真正转走 │ │ 商户开始等待到账 │ └──────────────────────┘ └──────────────────────┘为什么要分两步因为很多场景需要先确认用户有钱但暂时不扣酒店入住时授权 ¥2000退房时按实际消费 ¥1200 捕获电商预售下单时授权发货时才捕获订阅试用授权 $0.00 验证卡片有效试用期结束后捕获第一笔月费2.2 在 Stripe 中怎么用Stripe 的 Payment Intents 默认是授权后自动捕获——也就是说用户支付成功后立刻扣款。对于大多数场景这就够了。如果你需要手动控制捕获时机比如电商需要发货后再扣款只需要一个参数# 创建时设置手动捕获intentstripe.PaymentIntent.create(amount14999,currencyusd,capture_methodmanual,# 改成手动捕获)# 发货后手动捕获stripe.PaymentIntent.capture(pi_xxx)注意授权有有效期通常 7 天。超过有效期没有捕获授权会自动释放——钱回到用户卡里。三、3D Secure那个弹窗是怎么回事回到 John 的故事。他输入卡号后弹出了一个验证窗口要求输入短信验证码。这就是3D Secure3DS。3.1 3DS 是什么3DS 是 Visa、Mastercard 等卡组织推出的额外身份验证层。它在标准支付流程中插入了一步把用户重定向到发卡行的验证页面确认正在用卡的人真的是卡的主人。标准流程 John → 商户 → Stripe → Chase发卡行→ 扣款 3DS 增强 John → 商户 → Stripe → 【Chase 验证页面请输入短信验证码】→ 验证通过 → 扣款3.2 两种 3DS 流程现在主流的是3DS 2.0它有两种流程Frictionless Flow无感验证发卡行在后台分析了 John 的设备指纹、消费习惯、地理位置等数据判断这是一笔低风险交易自动通过。John 完全没感知到 3DS 的存在。Challenge Flow挑战验证发卡行觉得有风险比如 John 突然从一个从没用过的设备、在凌晨、购买了一件高价商品弹出验证窗口要求 John 输入短信验证码或进行指纹认证。对开发者来说最好的消息是在 Stripe 中3DS 是自动处理的。你调用stripe.confirmCardPayment()后如果发卡行要求 3DSStripe.js 会自动弹出验证窗口。你不需要写任何额外代码。3.3 欧洲 SCA 合规不是可选的如果你的用户在欧洲EEA3DS 不是建议使用而是法律强制要求。欧洲的 PSD2 法规要求所有在线支付必须进行强客户认证Strong Customer AuthenticationSCA。SCA 要求至少两种认证因素你知道的 你拥有的 你本身的3DS 2.0 是最主要的合规手段。但也有一些豁免条件条件是否需要 SCA消费者和商户都在 EEA✅ 必须金额 €30 且近 5 笔总额 €100❌ 豁免商户被用户加入白名单❌ 豁免收单机构判定为低风险交易❌ 豁免订阅的首次支付✅ 必须订阅的后续自动扣款❌ 豁免好消息Stripe 会自动为你处理 SCA 的触发和豁免判断。这也是为什么推荐使用 Payment Intents API而不是旧版 Charges API的核心原因。四、PCI DSS为什么卡号不能经过你的服务器John 在网页上输入了他的 Visa 卡号4242 4242 4242 4242。这个卡号是怎么传给 Stripe 的如果你以为是浏览器 → 你的服务器 → Stripe那你就要面对一个大麻烦了。4.1 PCI DSS 是什么PCI DSSPayment Card Industry Data Security Standard是由 Visa、Mastercard 等组织联合制定的信用卡数据安全标准。只要你的系统存储、处理或传输了信用卡数据就必须遵守。合规等级从低到高集成方式卡号是否经过你的服务器合规等级工作量Stripe Elements推荐❌ 不经过SAQ A-EP填一张问卷Stripe.js Token❌ 不经过SAQ A-EP填一张问卷自建表单 直接调 API✅ 经过SAQ D300 项安全审计4.2 Stripe Elements 的安全设计当你使用 Stripe Elements 时页面上的卡号输入框实际上是一个Stripe 托管的 iframe。John 输入的卡号直接从他的浏览器发送到 Stripe 的服务器——完全不经过你的服务器。John 的浏览器 ┌──────────────────────────────────────────┐ │ 你的网页 │ │ ┌──────────────────────────────────────┐│ │ │ Stripe Elements (iframe) ││ │ │ 卡号4242 4242 4242 4242 ││ ──直接发送──→ Stripe 服务器 │ │ 有效期12/28 CVC123 ││ ↓ │ └──────────────────────────────────────┘│ Token 化 │ │ ↓ │ [支付 $149.99] │ tok_xxxx │ │ ↓ └──────────────────────────────────────────┘ 返回给你的服务器 只有 Token没有卡号你的服务器拿到的只是一个 Token比如tok_1MqLRJ2eZvKYlo...用这个 Token 去调用 Stripe API 扣款。即使这个 Token 泄露了攻击者也无法用它做任何事——因为 Token 只能在你的账户内使用一次。这就是为什么用 Stripe Elements 只需要填一张问卷就能合规而自建表单要做 300 项审计——因为你的服务器从未碰过真实卡号。五、动手Stripe 信用卡支付全流程理论讲完了让我们来写代码。Stripe 的对接分为前端和后端两部分。5.1 接入准备第一步注册 Stripe 账号stripe.com ▼ 第二步获取 API 密钥 ├── Publishable Keypk_test_xxx—— 前端使用可公开 └── Secret Keysk_test_xxx—— 后端使用绝不暴露到前端 ▼ 第三步配置 Webhook Endpoint 获取 Signing Secretwhsec_xxx ▼ 第四步开始沙盒开发test 模式密钥自动启用沙盒Stripe 的密钥体系比支付宝/微信清晰得多密钥类型前缀用途安全级别Publishable Keypk_test_/pk_live_前端初始化 Stripe.js可公开Secret Keysk_test_/sk_live_后端 API 调用 绝密Restricted Keyrk_test_/rk_live_后端限定权限 绝密Webhook Secretwhsec_验证 Webhook 签名 绝密5.2 整体流程Stripe 的 Payment Intents 流程和支付宝/微信有一个关键区别前端直接和 Stripe 通信后端只负责创建 PaymentIntent 和处理 Webhook。John 的浏览器 你的后端服务器 Stripe API │ │ │ │ ① 点击「支付」 │ │ │────────────────────────→│ │ │ │ ② 创建 PaymentIntent │ │ │─────────────────────→│ │ │ ③ 返回 client_secret │ │ │←─────────────────────│ │ ④ 拿到 client_secret │ │ │←────────────────────────│ │ │ │ │ │ ⑤ Stripe.js 收集卡号直接发给 Stripe │ │─────────────────────────────────────────────→│ │ │ │ │ ⑥ 如需 3DS自动弹出验证窗口 │ │←─────────────────────────────────────────────│ │ ⑦ John 完成验证 │ │─────────────────────────────────────────────→│ │ │ │ │ ⑧ 前端收到支付结果 │ │ │←─────────────────────────────────────────────│ │ │ ⑨ Webhook 通知 │ │ │←─────────────────────│ │ │ ⑩ 更新订单状态 │关键点卡号从始至终不经过你的服务器。你的后端只处理client_secret和 Webhook。5.3 后端实现Python FastAPIimportstripeimportosfromfastapiimportFastAPI,Request,HTTPException appFastAPI()stripe.api_keyos.getenv(STRIPE_SECRET_KEY)WEBHOOK_SECRETos.getenv(STRIPE_WEBHOOK_SECRET)app.post(/api/pay/stripe/create-payment-intent)asyncdefcreate_payment_intent(request:Request):John 点击支付后后端创建一个 PaymentIntentbodyawaitrequest.json()try:intentstripe.PaymentIntent.create(amountint(float(body[amount])*100),# 和微信一样单位是分currencybody.get(currency,usd),payment_method_types[card],metadata{order_id:body[order_id],# 你的订单号user_id:body.get(user_id),},# 如果需要手动捕获先授权后扣款加上# capture_methodmanual,)# 返回 client_secret 给前端# ⚠️ client_secret 可以安全地传给前端它只能用来确认这一笔支付return{client_secret:intent.client_secret,payment_intent_id:intent.id,}exceptstripe.error.StripeErrorase:raiseHTTPException(status_code400,detailstr(e))5.4 前端实现Stripe.js Elements前端是 Stripe 对接中最精彩的部分——看看不到 50 行代码怎么实现一个安全的信用卡支付!-- 引入 Stripe.js —— 必须从 Stripe CDN 加载这是 PCI 合规要求 --scriptsrchttps://js.stripe.com/v3//scriptdividpayment-form!-- 这个 div 会被 Stripe Elements 接管变成一个安全的卡号输入框 --dividcard-element/divbuttonidpay-button支付 $149.99/buttondividpayment-result/div/div// 1. 初始化 Stripe.js用 Publishable Key可公开conststripeStripe(pk_test_your_publishable_key);constelementsstripe.elements();// 2. 创建卡号输入组件这是一个 Stripe 托管的 iframeconstcardElementelements.create(card,{style:{base:{fontSize:16px,color:#32325d,::placeholder:{color:#aab7c4},},},});cardElement.mount(#card-element);// 3. 处理支付document.getElementById(pay-button).addEventListener(click,async(){// 3a. 先从你的后端获取 client_secretconstresawaitfetch(/api/pay/stripe/create-payment-intent,{method:POST,headers:{Content-Type:application/json},body:JSON.stringify({amount:149.99,currency:usd,order_id:ORD_20260404001,}),});const{client_secret}awaitres.json();// 3b. Stripe.js 确认支付——卡号直接从 iframe 发送到 Stripe// 如果需要 3DSStripe.js 会自动弹出验证窗口const{paymentIntent,error}awaitstripe.confirmCardPayment(client_secret,{payment_method:{card:cardElement,billing_details:{name:John Doe,email:johnexample.com,},},});// 3c. 处理结果if(error){document.getElementById(payment-result).textContenterror.message;}elseif(paymentIntent.statussucceeded){document.getElementById(payment-result).textContent支付成功;}});就这么多。Stripe.js 在背后帮你处理了卡号传输、Token 化、3DS 弹窗——你的代码只需要关心client_secret和最终结果。5.5 Webhook 处理和支付宝/微信一样Stripe 也通过异步通知Webhook告诉你支付结果。但 Stripe 的验签简单得多——用 HMAC-SHA256SDK 一行代码搞定app.post(/api/pay/stripe/webhook)asyncdefstripe_webhook(request:Request):Stripe Webhook 处理payloadawaitrequest.body()sig_headerrequest.headers.get(Stripe-Signature)# 1. 验签——一行代码搞定try:eventstripe.Webhook.construct_event(payload,sig_header,WEBHOOK_SECRET)except(ValueError,stripe.error.SignatureVerificationError):raiseHTTPException(status_code400,detailInvalid signature)# 2. 处理不同事件ifevent[type]payment_intent.succeeded:intentevent[data][object]order_idintent[metadata][order_id]# 幂等检查orderawaitget_order(order_id)iforder.statusPAID:return{status:ok}awaitupdate_order_status(order_id,PAID,transaction_idintent[id],amountintent[amount]/100,# 分 → 元currencyintent[currency])elifevent[type]payment_intent.payment_failed:intentevent[data][object]order_idintent[metadata][order_id]error_msgintent.get(last_payment_error,{}).get(message,Unknown)awaitupdate_order_status(order_id,FAILED,errorerror_msg)return{status:ok}对比一下三大渠道的验签复杂度支付宝RSA 非对称验签需要公钥证书★★★☆微信支付RSA 验签 AES-GCM 解密最复杂★★★★StripeHMAC-SHA256SDK 一行代码★★☆☆Stripe 的回调重试策略在 72 小时内使用指数退避重试。你的 Webhook 端点需要在收到通知后返回 HTTP 200。六、Payment Intents 状态机理解 Payment Intents 的状态流转能帮你在调试时快速定位问题。Stripe PaymentIntent 状态流转 requires_payment_method ← 刚创建等待用户选择支付方式 │ ▼ requires_confirmation ← 等待前端确认 │ ├── 无需 3DS ─────────→ succeeded或 requires_capture │ └── 需要 3DS ─────────→ requires_action │ ▼ Stripe.js 自动弹出 3DS 验证窗口 │ ├── 验证成功 → succeeded ✅ └── 验证失败 → requires_payment_method ❌当你设置了capture_methodmanual时支付成功后状态会停在requires_capture而不是succeeded等你手动调用 capture 后才变为succeeded。七、争议与拒付信用卡最大的风险让我们回到 John 的故事。假设 John 收到鞋子后过了一周向 Chase 银行发起了一个争议——“我没在这个网站买过东西”也许是他的室友偷用了他的卡也许他就是想白嫖。这就是信用卡特有的机制——拒付Chargeback。7.1 争议处理流程John 向银行投诉 → Chase 冻结 $149.99 → Stripe 通知你 → 你提交证据 → Chase 裁决 ↑ 45 天内提交证据 发货证明、签收记录、聊天记录等Stripe 的争议状态会经历这些阶段warning_needs_response预警还没正式立案needs_response需要你在截止日期前提交证据under_review银行审核中won你赢了钱回来了 lost你输了钱没了 7.2 争议的代价每次争议Stripe 会收取$15 的争议处理费。好消息是如果你胜诉这 $15 会退还。败诉则不退。更大的风险是如果你的争议率超过 1%每 100 笔交易有超过 1 笔争议Stripe 可能会限制甚至关闭你的账户。Visa 和 Mastercard 也会把你列入高风险商户名单。7.3 如何减少争议✅ 开启 3D Secure——经过 3DS 验证的交易责任转移给发卡行 ✅ 让你的商户名在银行账单上清晰可辨别让 John 看到一个他不认识的名字 ✅ 发货后及时提供物流追踪号 ✅ 遇到退款请求时快速处理而不是拖着——拖到用户找银行投诉就变成争议了 ✅ 在显眼位置展示退换货政策八、退款相比争议退款要简单得多——你主动把钱退给用户。# 全额退款refundstripe.Refund.create(payment_intentpi_xxx,reasonrequested_by_customer)# 部分退款比如只退 $50refundstripe.Refund.create(payment_intentpi_xxx,amount5000,# 单位分)Stripe 退款的几个特点没有时间限制不像支付宝 3 个月、微信 1 年手续费退还不像 PayPal 退款不退手续费到账时间5~10 个工作日比国内慢因为要走国际清算网络有 Webhook 通知charge.refunded事件九、Stripe vs Adyen怎么选如果你做国际信用卡收单除了 Stripe另一个绑不开的名字是Adyen。简单对比选 Stripe 如果你是初创到中大型企业看重开发体验和文档质量主要做线上业务。Stripe 的文档堪称业界标杆——几乎所有问题都能在文档里找到答案。费率透明2.9% $0.30在 46 个国家/地区可注册。选 Adyen 如果你是中大型到超大型企业需要线上线下一体化POS 在线需要接入 200 种支付方式或者交易量足够大可以谈到更优惠的阶梯定价。对于大多数开发者和初创公司Stripe 是默认选择。十、测试Stripe 提供了一套完善的测试卡号你不需要真实信用卡就能在沙盒环境中测试卡号场景4242 4242 4242 4242正常支付成功4000 0025 0000 3155触发 3DS 验证4000 0000 0000 9995支付被拒余额不足4000 0000 0000 0002支付被拒卡被拒绝所有测试卡的有效期填任意未来日期CVC 填任意 3 位数字即可。本系列文章导读篇目标题你将学到第 1 篇概览篇——一笔订单触发的支付之旅四方模型、支付生命周期、市场格局、成本分析、选型决策第 2 篇支付宝 微信支付——一杯咖啡的扫码之旅签名机制、回调处理、AES-GCM 解密、完整对接代码第 3 篇Stripe 信用卡——一件跨境商品的卡支付之旅本文Payment Intents、3D Secure、PCI DSS、前后端代码第 4 篇PayPal——一位海外买家的安全支付之旅OAuth 认证、Smart Buttons、争议保护、Webhook第 5 篇统一支付网关——当四条河流汇入一片海三层架构、策略模式、幂等设计、对账补偿参考来源Stripe Documentation: Payment IntentsStripe Documentation: 3D SecureStripe Documentation: PCI ComplianceStripe Documentation: DisputesEuropean PSD2 SCA RequirementsEMVCo 3D Secure 2.0 SpecificationPCI Security Standards Council欢迎关注公众号coft获取更多深度技术文章。下一篇我们跟着伦敦的 Emma看看 PayPal 是怎么让她敢在一个陌生网站上花 $89 买手工包的。