1. 为什么选择Stripe出海支付的首选方案如果你正在开发一个面向海外用户的应用或者你的电商平台准备拓展国际市场那么支付环节绝对是你需要优先解决的核心问题。我做了这么多年技术对接过不少支付渠道说实话Stripe在海外市场的地位有点像国内的支付宝和微信支付属于“基础设施”级别的存在。它之所以成为众多出海开发者的首选原因其实很直接开箱即用、文档齐全、生态成熟。对于刚接触的开发者来说Stripe官网那一堆概念——PaymentIntent、Session、Price、Product、Webhook——确实容易让人眼花缭乱。但别慌这恰恰说明它的功能设计得非常细致和灵活能覆盖从一次性购买到复杂订阅、从简单收款到多步骤结账的几乎所有场景。我们这次要做的就是用SpringBoot这个国内开发者最熟悉的框架一步步把这些概念串起来搭建一个从测试到上线都稳稳当当的支付流程。我自己的经验是Stripe的沙箱环境也就是测试环境做得非常友好完全模拟真实交易但又不会产生任何实际资金流动。这让我们可以在一个安全的环境里反复调试把各种边界情况都测一遍比如支付成功、失败、用户中途取消甚至是网络超时重试。等所有逻辑都跑通了切换到生产环境就是改个API密钥的事儿心里特别有底。所以这篇文章我会把我自己踩过的坑、总结的最佳实践以及那些官方文档里没明说但很重要的细节都揉碎了讲给你听。目标就一个让你看完就能动手把Stripe支付稳稳地集成到自己的SpringBoot应用里。2. 环境准备与项目初始化打好地基万事开头难但把环境准备好后面就顺了。这一步我们要做两件关键事一是在Stripe后台创建好你的“工作空间”二是在SpringBoot项目里引入正确的依赖。2.1 在Stripe Dashboard上获取你的密钥首先你得去 Stripe官网 注册一个账号。这个过程很简单跟着引导走就行。注册成功后你会进入一个叫做Dashboard的管理后台。这里是你管理所有支付相关设置的控制台。登录Dashboard后注意看页面左上角通常会有一个环境切换的选项比如显示着“Test mode”测试模式。请确保你当前处于测试模式。在这个模式下所有操作都是模拟的不会产生真实交易。接下来找到获取API密钥的地方。一般在左侧菜单栏找到“Developers”点击进入后你会看到“API keys”选项卡。这里你会看到两对至关重要的密钥Publishable key (pk_xxx)可发布密钥。这个密钥是用于前端的相对安全因为它的权限有限主要用于创建支付会话等操作。你可以把它放在前端代码或配置文件中。Secret key (sk_xxx)秘密密钥。这是你的命根子绝对不能泄露它拥有最高权限用于后端与Stripe服务器进行敏感交互比如创建支付意图、处理Webhook签名验证等。这个密钥必须妥善保存在服务器的环境变量或安全的配置中心绝不能提交到代码仓库。在测试模式下你会看到pk_test_xxx和sk_test_xxx这样的密钥。把它们复制下来待会儿我们要用到。当你一切准备就绪要上线时只需要在Dashboard左上角切换到“Live mode”生产模式这里就会生成一对新的pk_live_xxx和sk_live_xxx密钥用于真实的资金交易。切记在测试阶段绝对不要误用生产环境的密钥。2.2 创建SpringBoot项目并引入依赖打开你熟悉的IDE比如IntelliJ IDEA创建一个新的SpringBoot项目。选择你常用的依赖比如Spring Web、Lombok简化代码等。然后打开pom.xml文件添加Stripe的Java SDK依赖。这里有个关键点SDK版本。Stripe的API迭代比较快网上有些老教程用的版本可能已经过时导致某些方法找不到。我写这篇文章时基于当前实践稳定且功能齐全的版本是26.3.0。建议你使用这个或更高的小版本。dependency groupIdcom.stripe/groupId artifactIdstripe-java/artifactId version26.3.0/version /dependency添加依赖后在项目的application.yml或application.properties配置文件中加入我们刚才获取的密钥stripe: api: # 测试环境的秘密密钥从Dashboard获取 secret-key: sk_test_xxxxxxxxxxxxxxxxxxxxxxxx # 测试环境的可发布密钥前端可能会用到 publishable-key: pk_test_xxxxxxxxxxxxxxxxxxxxxxxx为了在代码中方便地使用这些配置我们可以创建一个配置类import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; Component ConfigurationProperties(prefix stripe.api) Data public class StripeConfig { private String secretKey; private String publishableKey; }最后我强烈建议你创建一个全局的配置初始化类在应用启动时设置Stripe的API密钥。这样在后续任何地方使用Stripe SDK的静态方法时都不用再重复设置了。import com.stripe.Stripe; import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; Configuration RequiredArgsConstructor public class StripeInitializer { private final StripeConfig stripeConfig; PostConstruct public void init() { // 设置全局的Stripe API密钥 Stripe.apiKey stripeConfig.getSecretKey(); } }好了到这里我们的开发地基就打得差不多了。密钥有了依赖引入了配置也加载了。接下来我们就要进入最核心的业务逻辑部分创建商品价格和发起支付。3. 核心支付流程开发从创建商品到生成支付链接支付流程的核心可以概括为“定价格建会话拿链接”。听起来简单但每一步都有需要注意的细节尤其是如何把我们的业务数据比如订单号安全地传递并关联到支付过程中。3.1 创建商品与价格Price在Stripe的世界里用户最终支付的对象是一个Price价格对象。一个Price必须关联一个Product商品。你可以把Product理解为你的商品或服务本身比如“月度会员”而Price则是这个商品在某个特定时间点的定价比如“月度会员 - 9.99美元”。为什么需要先创建Price因为Stripe鼓励你将价格信息标准化和预定义。这样在创建支付会话时你只需要引用这个Price的ID而不是每次都传递金额、货币和商品名既减少了出错概率也方便后续统一管理价格变动。我们来写一个创建Price的服务方法。首先定义一个简单的参数对象来接收前端传来的价格信息import lombok.Data; Data public class CreatePriceRequest { // 商品名称如 Premium Plan - Monthly private String productName; // 金额单位为对应货币的最小单位如美元是美分100表示1.00美元 private Long unitAmount; // 货币代码如 usd, eur private String currency; }然后在服务层实现创建逻辑。这里有个小技巧同一个商品Product可以创建多个不同价格的Price对象比如针对不同地区或不同促销活动。Price创建后会产生一个形如price_1Pq...的唯一ID这个ID就是我们后续发起支付的凭证。import com.stripe.exception.StripeException; import com.stripe.model.Price; import com.stripe.param.PriceCreateParams; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; Service Slf4j RequiredArgsConstructor public class StripePriceService { public String createPrice(CreatePriceRequest request) throws StripeException { // 构建创建Price的参数 PriceCreateParams params PriceCreateParams.builder() .setCurrency(request.getCurrency().toLowerCase()) // 货币单位需小写 .setUnitAmount(request.getUnitAmount()) // 金额注意单位 .setProductData( // 内联创建商品信息 PriceCreateParams.ProductData.builder() .setName(request.getProductName()) .build() ) .build(); // 调用Stripe API创建价格 Price price Price.create(params); String priceId price.getId(); log.info(Price created successfully. ID: {}, Amount: {}{}, priceId, request.getUnitAmount(), request.getCurrency()); // 将这个priceId存入你的数据库与你的业务商品关联 // yourProductService.savePriceId(yourProductId, priceId); return priceId; } }重要提示unitAmount是以货币最小单位为单位的整数。比如美元USD和美分1美元 100美分。如果你想收取19.99美元这里的unitAmount应该是1999。日元JPY没有小数所以1000日元就是1000。这个细节千万不能搞错否则金额就对不上了。创建Price通常不是每次支付前都要做的操作。它更像是一个商品上架或价格调整时的管理操作。一旦创建好这个priceId就可以反复使用直到你创建新的Price来覆盖它。3.2 创建支付会话Checkout Session并传递业务参数有了priceId我们就可以引导用户去支付了。Stripe推荐使用Checkout来构建支付页面它是一个由Stripe托管、高度定制化且符合PCI DSS安全标准的支付流程页面。我们的后端需要做的就是创建一个Checkout Session支付会话然后把生成的Session URL给前端让用户跳转过去完成支付。创建Session的核心在于PaymentIntent支付意图。你可以把PaymentIntent理解为一次支付尝试的“总管”它跟踪支付的整个状态流待处理、成功、失败等。我们在创建Session时会在其中嵌入一个PaymentIntent。这里有一个极其关键的实践如何将我们自己的业务标识比如订单号order_123456与Stripe的支付关联起来答案就是使用Metadata元数据。Metadata是Stripe提供的一个键值对字段你可以存放任何自定义信息它会在Webhook回调中原封不动地传回给你这是打通两个系统数据关联的生命线。下面是一个完整的创建支付会话的服务方法import com.stripe.exception.StripeException; import com.stripe.model.checkout.Session; import com.stripe.param.checkout.SessionCreateParams; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; Service Slf4j RequiredArgsConstructor public class StripePaymentService { // 从配置文件中读取前端域名用于构建回调地址 Value(${app.frontend.domain}) private String frontendDomain; /** * 创建Stripe支付会话 * param priceId 预创建的价格ID * param orderNo 你自己的业务订单号 * return 包含支付链接的Session对象 */ public Session createCheckoutSession(String priceId, String orderNo) throws StripeException { // 构建成功和取消支付后的回调地址 // 这里通常跳转回你自己的前端页面展示支付结果 String successUrl frontendDomain /payment/success?session_id{CHECKOUT_SESSION_ID}; String cancelUrl frontendDomain /payment/cancel; // 构建Session创建参数这是最核心的部分 SessionCreateParams params SessionCreateParams.builder() .setMode(SessionCreateParams.Mode.PAYMENT) // 模式一次性支付。如果是订阅用 SUBSCRIPTION .setSuccessUrl(successUrl) // 用户支付成功后的跳转地址 .setCancelUrl(cancelUrl) // 用户取消支付后的跳转地址 .addLineItem( // 添加支付项目 SessionCreateParams.LineItem.builder() .setQuantity(1L) // 购买数量 .setPrice(priceId) // 关联我们之前创建的Price .build() ) .setPaymentIntentData( // 配置关联的PaymentIntent SessionCreateParams.PaymentIntentData.builder() // !!! 核心将业务订单号存入Metadata !!! .putMetadata(order_no, orderNo) // 你还可以放入其他信息比如用户ID、商品ID等 // .putMetadata(user_id, userId) .build() ) .build(); // 调用API创建会话 Session session Session.create(params); log.info(Checkout Session created for order {}. Session ID: {}, URL: {}, orderNo, session.getId(), session.getUrl()); // 这里你可以将 session.getId() 与你自己的订单关联存入数据库方便后续查询 // orderService.updateStripeSessionId(orderNo, session.getId()); return session; } }创建成功后session.getUrl()就是一个指向Stripe托管支付页面的链接。前端拿到这个URL后可以直接用window.location.href跳转或者嵌入到按钮的点击事件中。用户会被引导到一个类似下图的专业支付页面输入卡号等信息完成支付。在测试环境你可以使用卡号4242 4242 4242 4242任意未来日期作为有效期任意三位数作为CVC来模拟一次成功的支付。支付完成后用户会根据结果被重定向到你设置的successUrl或cancelUrl。但请注意前端重定向并不可靠用户可能关闭页面网络可能中断它不能作为更新订单状态的唯一依据。支付状态更新的“真理”来源必须是后端可靠接收的Webhook 回调。这就是我们下一章要解决的核心问题。4. 构建可靠的Webhook端点确保支付状态万无一失Webhook是Stripe主动向你服务器发送的HTTP通知告诉你支付过程中发生的各种事件。它是确保数据最终一致性的基石。想象一下用户付了钱但因为网络问题没跳转回你的成功页面如果没有Webhook这笔订单在你的系统里可能永远处于“待支付”状态。Webhook就是来解决这个问题的。4.1 在Stripe Dashboard中配置Webhook首先你需要告诉Stripe“发生事件时请通知我这个地址”。有两种配置方式1. 通过Dashboard页面配置推荐给新手在Stripe Dashboard侧边栏找到“Developers” - “Webhooks”。点击“Add endpoint”。Endpoint URL: 填写你后端提供的、能处理POST请求的API地址例如https://yourdomain.com/api/stripe/webhook。在开发阶段你可以使用内网穿透工具如ngrok将本地服务暴露为一个公网地址来测试。Events to send: 选择需要监听的事件。对于基础支付流程我建议至少勾选checkout.session.completed(支付会话完成)payment_intent.succeeded(支付意图成功)payment_intent.payment_failed(支付意图失败)点击“Add endpoint”创建。创建成功后你会看到一个“Signing secret”形如whsec_xxxx。立即复制并保存好这个密钥它用于验证Webhook请求的真实性防止伪造请求。2. 通过代码创建适合自动化部署你也可以用API动态创建Webhook端点这在某些CI/CD场景下有用。但大多数情况下在Dashboard配置一次就够了。4.2 实现SpringBoot Webhook控制器这是后端最关键的代码之一。它需要做三件事验证请求签名、解析事件、根据事件类型处理业务。import com.stripe.Stripe; import com.stripe.exception.SignatureVerificationException; import com.stripe.model.*; import com.stripe.net.Webhook; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; import java.io.BufferedReader; import java.io.IOException; RestController RequestMapping(/api/stripe) Slf4j RequiredArgsConstructor public class StripeWebhookController { // 从配置文件注入Webhook签名密钥 Value(${stripe.webhook.secret}) private String webhookSecret; PostMapping(/webhook) public void handleWebhook(HttpServletRequest request, HttpServletResponse response) throws IOException { // 1. 读取请求体Payload BufferedReader reader request.getReader(); StringBuilder payloadBuilder new StringBuilder(); String line; while ((line reader.readLine()) ! null) { payloadBuilder.append(line); } String payload payloadBuilder.toString(); // 获取Stripe发送的签名头 String sigHeader request.getHeader(Stripe-Signature); Event event null; String orderNo null; try { // 2. 核心安全步骤验证Webhook签名 // 确保请求确实来自Stripe而不是恶意第三方 event Webhook.constructEvent(payload, sigHeader, webhookSecret); } catch (SignatureVerificationException e) { // 签名验证失败记录日志并返回401 log.error(⚠️ Webhook signature verification failed. Potential malicious request.); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 return; } catch (Exception e) { log.error(❌ Webhook processing error., e); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); // 400 return; } // 3. 根据事件类型进行处理 String eventType event.getType(); log.info(Received Stripe webhook event: {}, eventType); // 从事件数据中提取我们之前存入的Metadata EventDataObjectDeserializer dataObjectDeserializer event.getDataObjectDeserializer(); StripeObject stripeObject null; if (dataObjectDeserializer.getObject().isPresent()) { stripeObject dataObjectDeserializer.getObject().get(); } // 提取我们自定义的订单号 if (stripeObject instanceof PaymentIntent) { PaymentIntent paymentIntent (PaymentIntent) stripeObject; orderNo paymentIntent.getMetadata().get(order_no); } else if (stripeObject instanceof Session) { Session session (Session) stripeObject; // 有时订单号也可能放在Session关联的PaymentIntent里这里根据你的设置来 // 示例中我们从PaymentIntent取所以这里可能不需要 } // 4. 事件分发与业务处理 try { switch (eventType) { case checkout.session.completed: // 支付会话完成但此时资金可能还未完全结算 log.info(Session completed for order: {}, orderNo); // 可以更新订单状态为“已付款待确认”等 // orderService.updateStatus(orderNo, OrderStatus.PAID_PENDING); break; case payment_intent.succeeded: // 最重要的成功事件 // 资金已确认收到订单最终成功 log.info(Payment succeeded for order: {}, orderNo); if (orderNo ! null) { // 调用你的业务服务将订单状态更新为“支付成功” // 这里应该包含幂等性处理防止Webhook重复调用导致业务重复处理 orderService.confirmPaymentSuccess(orderNo); } break; case payment_intent.payment_failed: // 支付失败如卡被拒 log.warn(Payment failed for order: {}, orderNo); if (orderNo ! null) { orderService.markPaymentFailed(orderNo); } break; // 可以根据需要处理更多事件 case charge.refunded: log.info(Charge refunded for order: {}, orderNo); break; default: log.debug(Unhandled event type: {}, eventType); break; } // 处理成功返回200 OK给Stripe response.setStatus(HttpServletResponse.SC_OK); } catch (Exception e) { // 业务处理逻辑出错 log.error(❌ Business logic error while processing webhook for event {} and order {}, eventType, orderNo, e); // 返回500错误Stripe会根据重试策略稍后重新发送这个事件 response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } } }几个必须注意的要点签名验证是生命线Webhook.constructEvent()这行代码绝对不能省略。它使用你配置的webhookSecret来验证请求是否真的来自Stripe防止攻击者伪造支付成功通知来欺骗你的系统。幂等性处理Stripe为了保证送达可能会多次发送同一个事件的Webhook。你的业务处理逻辑如orderService.confirmPaymentSuccess必须是幂等的。简单说就是同一笔订单无论收到多少次“成功”通知最终都只生效一次。可以在数据库订单表里加一个“支付状态”字段只有状态是“待支付”时才更新为“成功”。及时返回HTTP状态码处理成功返回200 OK处理失败业务逻辑错误返回500等错误码。Stripe看到非2xx状态码会在未来一段时间内最多72小时以指数退避的方式重试发送直到你返回成功为止。这保证了事件的最终可达性。Metadata是桥梁我们正是通过paymentIntent.getMetadata().get(order_no)取回了创建支付会话时埋下的订单号从而将Stripe的支付事件与你的业务订单关联起来。4.3 本地测试Webhook在开发时你的本地服务没有公网地址Stripe无法直接回调。你可以使用Stripe CLI工具来轻松解决。安装并登录CLI后运行一条命令就能将Stripe的事件转发到你的本地服务端口stripe listen --forward-to localhost:8080/api/stripe/webhook这条命令会给你生成一个本地测试用的Webhook签名密钥把它配置到你的application.yml里。然后在Dashboard的测试模式触发支付事件就会自动转发到你的本地服务调试起来非常方便。5. 从沙箱到生产上线前的最后检查当你在沙箱环境测试得滚瓜烂熟所有支付成功、失败、取消的场景都处理妥当后就可以准备上线了。切换生产环境不是简单地换个密钥有几个关键步骤需要确认。第一步切换Dashboard环境与密钥在Stripe Dashboard左上角将模式从“Test mode”切换到“Live mode”。你会看到全新的、以pk_live_和sk_live_开头的API密钥。在你的生产服务器环境变量或配置中心将stripe.api.secret-key和stripe.api.publishable-key的值更新为这组新的生产密钥。务必确保你的代码中没有硬编码密钥并且测试环境的密钥没有误传到生产服务器。第二步配置生产环境的Webhook在Dashboard的Live mode下你需要为生产环境重新配置一个Webhook端点。步骤和测试时一样进入Developers - Webhooks。点击“Add endpoint”。输入你生产服务器的公网API地址例如https://api.your-real-app.com/api/stripe/webhook。选择需要监听的事件和测试环境保持一致或根据业务增加。获取并保存新的Live Signing Secret并更新到生产服务器的配置中。第三步全面功能与监控验证发起一笔真实的小额交易使用一张真实有效的国际信用卡如Visa/Mastercard双币卡测试整个支付流程。金额可以设置得非常小比如1美元Stripe会收取少量手续费但这是上线前最真实的验证。验证Webhook接收完成小额支付后立即检查你的服务器日志确认收到了payment_intent.succeeded等生产环境的事件并且业务逻辑正确执行订单状态更新、库存扣减等。检查仪表盘在Dashboard的Live mode下查看Payments列表确认交易记录清晰可见状态正确。设置告警在Dashboard中配置邮件或Slack告警监控支付失败率、争议Dispute等关键指标。第四步处理可能的生产环境差异验证规则Verification生产环境对卡号的验证更严格比如进行CVC检查和地址邮编验证。确保你的支付表单或Checkout配置能妥善处理这些验证失败的情况给用户友好的提示。争议与退款提前了解Stripe的争议处理流程并在你的管理后台准备好手动退款的入口。熟悉charge.dispute.created等Webhook事件的处理。日志与审计生产环境的日志需要更详细、更结构化方便出了问题快速定位。建议将Stripe的事件ID、支付意图ID等关键信息与你自己的订单ID关联记录。走完这几步你的Stripe支付集成就算真正在生产环境跑起来了。整个过程的核心思想就是沙箱环境充分模拟核心逻辑创建会话、处理Webhook一次写好上线时主要切换配置和验证通道。把Webhook的可靠性和Metadata的数据关联做好整个支付闭环就牢固了。支付系统是业务的命脉多花点时间在测试和验证上绝对值得。