Stripe 是一家提供支付处理服务的公司,它允许商家轻松地接受在线支付。以下是关于 Stripe 的一些详细信息,涵盖了其功能、使用场景和优势:### Stripe 简介- **成立时间**:2010年- **总部**:美国旧金山- **服务范围**:全

摘要:在线支付系列(三):Stripe & 信用卡——一件跨境商品的卡支付之旅 $149.99 的限量球鞋,3 秒钟经历了什么? 纽约的 John 深夜刷手机,在一个中国独立站上发现了一双限量球鞋,标价 $149.99。他
在线支付系列(三):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 默认是授权后自动捕获——也就是说用户支付成功后立刻扣款。对于大多数场景,这就够了。 如果你需要手动控制捕获时机(比如电商需要发货后再扣款),只需要一个参数: # 创建时设置手动捕获 intent = stripe.PaymentIntent.create( amount=14999, currency="usd", capture_method="manual", # 改成手动捕获 ) # 发货后,手动捕获 stripe.PaymentIntent.capture("pi_xxx") 注意:授权有有效期(通常 7 天)。超过有效期没有捕获,授权会自动释放——钱回到用户卡里。 三、3D Secure:那个弹窗是怎么回事? 回到 John 的故事。他输入卡号后弹出了一个验证窗口,要求输入短信验证码。这就是 3D Secure(3DS)。 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() 后,如果发卡行要求 3DS,Stripe.js 会自动弹出验证窗口。你不需要写任何额外代码。 3.3 欧洲 SCA 合规:不是可选的 如果你的用户在欧洲(EEA),3DS 不是"建议使用",而是法律强制要求。 欧洲的 PSD2 法规要求所有在线支付必须进行强客户认证(Strong Customer Authentication,SCA)。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 DSS(Payment Card Industry Data Security Standard)是由 Visa、Mastercard 等组织联合制定的信用卡数据安全标准。只要你的系统存储、处理或传输了信用卡数据,就必须遵守。 合规等级从低到高: 集成方式 卡号是否经过你的服务器 合规等级 工作量 Stripe Elements(推荐) ❌ 不经过 SAQ A-EP 填一张问卷 Stripe.js + Token ❌ 不经过 SAQ A-EP 填一张问卷 自建表单 + 直接调 API ✅ 经过 SAQ D 300+ 项安全审计 4.2 Stripe Elements 的安全设计 当你使用 Stripe Elements 时,页面上的卡号输入框实际上是一个 Stripe 托管的 iframe。John 输入的卡号直接从他的浏览器发送到 Stripe 的服务器——完全不经过你的服务器。 John 的浏览器 ┌──────────────────────────────────────────┐ │ 你的网页 │ │ ┌──────────────────────────────────────┐│ │ │ Stripe Elements (iframe) ││ │ │ 卡号:4242 4242 4242 4242 ││ ──直接发送──→ Stripe 服务器 │ │ 有效期:12/28 CVC:123 ││ ↓ │ └──────────────────────────────────────┘│ 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 Key(pk_test_xxx)—— 前端使用,可公开 └── Secret Key(sk_test_xxx)—— 后端使用,绝不暴露到前端 ▼ 第三步:配置 Webhook Endpoint + 获取 Signing Secret(whsec_xxx) ▼ 第四步:开始沙盒开发(test 模式密钥自动启用沙盒) Stripe 的密钥体系比支付宝/微信清晰得多: 密钥类型 前缀 用途 安全级别 Publishable Key pk_test_ / pk_live_ 前端初始化 Stripe.js 可公开 Secret Key sk_test_ / sk_live_ 后端 API 调用 🔴 绝密 Restricted Key rk_test_ / rk_live_ 后端,限定权限 🔴 绝密 Webhook Secret whsec_ 验证 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 + FastAPI) import stripe import os from fastapi import FastAPI, Request, HTTPException app = FastAPI() stripe.api_key = os.getenv("STRIPE_SECRET_KEY") WEBHOOK_SECRET = os.getenv("STRIPE_WEBHOOK_SECRET") @app.post("/api/pay/stripe/create-payment-intent") async def create_payment_intent(request: Request): """John 点击支付后,后端创建一个 PaymentIntent""" body = await request.json() try: intent = stripe.PaymentIntent.create( amount=int(float(body["amount"]) * 100), # 和微信一样,单位是分! currency=body.get("currency", "usd"), payment_method_types=["card"], metadata={ "order_id": body["order_id"], # 你的订单号 "user_id": body.get("user_id"), }, # 如果需要手动捕获(先授权后扣款),加上: # capture_method="manual", ) # 返回 client_secret 给前端 # ⚠️ client_secret 可以安全地传给前端,它只能用来确认这一笔支付 return { "client_secret": intent.client_secret, "payment_intent_id": intent.id, } except stripe.error.StripeError as e: raise HTTPException(status_code=400, detail=str(e)) 5.4 前端实现(Stripe.js + Elements) 前端是 Stripe 对接中最精彩的部分——看看不到 50 行代码怎么实现一个安全的信用卡支付: <!-- 引入 Stripe.js —— 必须从 Stripe CDN 加载,这是 PCI 合规要求 --> <script src="https://js.stripe.com/v3/"></script> <div id="payment-form"> <!-- 这个 div 会被 Stripe Elements 接管,变成一个安全的卡号输入框 --> <div id="card-element"></div> <button id="pay-button">支付 $149.99</button> <div id="payment-result"></div> </div> // 1. 初始化 Stripe.js(用 Publishable Key,可公开) const stripe = Stripe('pk_test_your_publishable_key'); const elements = stripe.elements(); // 2. 创建卡号输入组件(这是一个 Stripe 托管的 iframe) const cardElement = elements.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_secret const res = await fetch('/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 } = await res.json(); // 3b. Stripe.js 确认支付——卡号直接从 iframe 发送到 Stripe // 如果需要 3DS,Stripe.js 会自动弹出验证窗口 const { paymentIntent, error } = await stripe.confirmCardPayment( client_secret, { payment_method: { card: cardElement, billing_details: { name: 'John Doe', email: 'john@example.com', }, }, } ); // 3c. 处理结果 if (error) { document.getElementById('payment-result').textContent = error.message; } else if (paymentIntent.status === 'succeeded') { document.getElementById('payment-result').textContent = '支付成功!🎉'; } }); 就这么多。Stripe.js 在背后帮你处理了卡号传输、Token 化、3DS 弹窗——你的代码只需要关心 client_secret 和最终结果。 5.5 Webhook 处理 和支付宝/微信一样,Stripe 也通过异步通知(Webhook)告诉你支付结果。但 Stripe 的验签简单得多——用 HMAC-SHA256,SDK 一行代码搞定: @app.post("/api/pay/stripe/webhook") async def stripe_webhook(request: Request): """Stripe Webhook 处理""" payload = await request.body() sig_header = request.headers.get("Stripe-Signature") # 1. 验签——一行代码搞定 try: event = stripe.Webhook.construct_event( payload, sig_header, WEBHOOK_SECRET ) except (ValueError, stripe.error.SignatureVerificationError): raise HTTPException(status_code=400, detail="Invalid signature") # 2. 处理不同事件 if event["type"] == "payment_intent.succeeded": intent = event["data"]["object"] order_id = intent["metadata"]["order_id"] # 幂等检查 order = await get_order(order_id) if order.status == "PAID": return {"status": "ok"} await update_order_status( order_id, "PAID", transaction_id=intent["id"], amount=intent["amount"] / 100, # 分 → 元 currency=intent["currency"] ) elif event["type"] == "payment_intent.payment_failed": intent = event["data"]["object"] order_id = intent["metadata"]["order_id"] error_msg = intent.get("last_payment_error", {}).get("message", "Unknown") await update_order_status(order_id, "FAILED", error=error_msg) return {"status": "ok"} 对比一下三大渠道的验签复杂度: 支付宝:RSA 非对称验签,需要公钥证书(★★★☆) 微信支付:RSA 验签 + AES-GCM 解密,最复杂(★★★★) Stripe:HMAC-SHA256,SDK 一行代码(★★☆☆) 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_method="manual" 时,支付成功后状态会停在 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 看到一个他不认识的名字) ✅ 发货后及时提供物流追踪号 ✅ 遇到退款请求时,快速处理而不是拖着——拖到用户找银行投诉就变成争议了 ✅ 在显眼位置展示退换货政策 八、退款 相比争议,退款要简单得多——你主动把钱退给用户。 # 全额退款 refund = stripe.Refund.create( payment_intent="pi_xxx", reason="requested_by_customer" ) # 部分退款(比如只退 $50) refund = stripe.Refund.create( payment_intent="pi_xxx", amount=5000, # 单位:分 ) 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 Intents Stripe Documentation: 3D Secure Stripe Documentation: PCI Compliance Stripe Documentation: Disputes European PSD2 SCA Requirements EMVCo 3D Secure 2.0 Specification PCI Security Standards Council 欢迎关注公众号 coft,获取更多深度技术文章。下一篇,我们跟着伦敦的 Emma,看看 PayPal 是怎么让她敢在一个陌生网站上花 $89 买手工包的。