Stripe 支付集成入门实践

109 阅读9分钟

Stripe 是一个为互联网业务设计的在线支付处理平台,提供完整的支付 API 和开发者工具。本文将介绍 Stripe 的核心概念、集成方式和最佳实践,帮助开发者快速在应用中接入支付功能。

Stripe 简介

Stripe 是一个为互联网业务提供支付处理的平台,通过 API 让开发者快速集成支付功能。

Stripe 成立于 2010 年,为全球数百万家企业提供支付服务,包括 Amazon、Shopify、Lyft 等。Stripe 已通过 PCI DSS Level 1 认证,支持 135+ 种货币和 100+ 种支付方式(信用卡、Apple Pay、微信支付、支付宝等)。

Stripe 的核心优势:

  • 开发者友好:RESTful API 设计简洁,提供多语言 SDK(JavaScript、Python、Ruby、Java 等)
  • 文档完善:官方文档详尽,提供丰富的代码示例和集成指南
  • 安全合规:Stripe 处理敏感支付信息,开发者无需直接接触信用卡数据
  • 功能丰富:支持一次性支付、订阅、分账(Stripe Connect)等多种场景

Stripe 特别适合 SaaS 应用、电商平台、订阅服务等需要灵活支付集成的场景。

参考链接:

核心概念

Stripe 的支付流程围绕几个核心对象构建。理解这些对象及其关系是集成 Stripe 的基础。

Payment Intent(支付意图)

PaymentIntent 是 Stripe 现代支付 API 的核心对象,代表一次支付的完整生命周期。根据 Stripe 官方文档,PaymentIntent 追踪支付从创建到成功的整个过程,自动处理 3D Secure 等复杂的认证流程。

// 创建 PaymentIntent
const paymentIntent = await stripe.paymentIntents.create({
  amount: 2000, // 金额(分为单位)
  currency: "usd",
  payment_method_types: ["card"], // 支持的支付方式
});

PaymentIntent 的状态流转:

  • requires_payment_method: 需要提供支付方式
  • requires_confirmation: 需要确认支付
  • requires_action: 需要额外操作(如 3D Secure)
  • processing: 处理中
  • succeeded: 支付成功
  • canceled: 已取消

Customer(客户)

Customer 对象用于存储客户信息,支持保存支付方式、管理订阅等。根据 Stripe API 文档,Customer 对象可以关联多个支付方式。

// 创建 Customer
const customer = await stripe.customers.create({
  email: "customer@example.com",
  name: "John Doe",
});

Payment Method(支付方式)

PaymentMethod 对象代表客户的支付工具(信用卡、银行账户等)。根据 Stripe API 文档,PaymentMethod 可以附加到 Customer 或 PaymentIntent。

// 创建 PaymentMethod
const paymentMethod = await stripe.paymentMethods.create({
  type: "card",
  card: {
    number: "4242424242424242",
    exp_month: 12,
    exp_year: 2025,
    cvc: "123",
  },
});

对象关系

Customer (客户)
  ├─ PaymentMethod (支付方式1)
  ├─ PaymentMethod (支付方式2)
  └─ PaymentIntent (支付意图)
       └─ Charge (实际扣款记录)
  • Customer 可以有多个 PaymentMethod
  • PaymentIntent 关联一个 Customer 和一个 PaymentMethod
  • PaymentIntent 成功后会自动创建 Charge 对象

集成方式

Stripe 提供三种主要的集成方式,适应不同的开发需求和时间成本。

Stripe Checkout(托管支付页面)

Stripe Checkout 是托管的支付页面,由 Stripe 提供 UI 和处理逻辑。根据 Stripe Checkout 文档,这是最快的集成方式。

// 后端:创建 Checkout Session
const session = await stripe.checkout.sessions.create({
  payment_method_types: ["card"],
  line_items: [
    {
      price_data: {
        currency: "usd",
        product_data: { name: "T-shirt" },
        unit_amount: 2000,
      },
      quantity: 1,
    },
  ],
  mode: "payment",
  success_url: "https://example.com/success",
  cancel_url: "https://example.com/cancel",
});

// 前端:重定向到 Stripe 托管页面
window.location.href = session.url;

优点:

  • 开发速度快,几行代码完成集成
  • UI 由 Stripe 维护,支持多语言
  • 自动处理 3D Secure、Apple Pay 等
  • PCI 合规性由 Stripe 负责

缺点:

  • UI 定制受限
  • 需要页面跳转

Stripe Elements(自定义 UI)

Stripe Elements 提供预构建的 UI 组件,可嵌入到自己的页面中。根据 Stripe Elements 文档,Elements 平衡了定制性和开发复杂度。

<!-- 前端:嵌入 Stripe Elements -->
<form id="payment-form">
  <div id="card-element"></div>
  <button type="submit">支付</button>
</form>

<script>
  const stripe = Stripe("pk_test_...");
  const elements = stripe.elements();
  const cardElement = elements.create("card");
  cardElement.mount("#card-element");

  // 提交表单
  const form = document.getElementById("payment-form");
  form.addEventListener("submit", async (e) => {
    e.preventDefault();

    // 后端创建 PaymentIntent
    const response = await fetch("/create-payment-intent", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ amount: 2000 }),
    });
    const { clientSecret } = await response.json();

    // 前端确认支付
    const result = await stripe.confirmCardPayment(clientSecret, {
      payment_method: { card: cardElement },
    });

    if (result.error) {
      console.error(result.error.message);
    } else {
      console.log("支付成功");
    }
  });
</script>

优点:

  • UI 可以定制样式
  • 用户无需离开当前页面
  • 仍然处理敏感数据的安全性

缺点:

  • 需要编写更多前端代码
  • 需要后端 API 配合

Payment Intents API(完全控制)

直接使用 Payment Intents API,完全自定义支付流程。根据 Payment Intents API 文档,这种方式提供最大灵活性。

// 后端:完整的支付流程控制
const paymentIntent = await stripe.paymentIntents.create({
  amount: 2000,
  currency: "usd",
  payment_method: "pm_card_visa", // 已保存的支付方式
  customer: "cus_xxx", // 关联客户
  confirm: true, // 立即确认
  return_url: "https://example.com/return", // 3DS 认证后的返回地址
});

// 检查支付状态
if (paymentIntent.status === "succeeded") {
  console.log("支付成功");
} else if (paymentIntent.status === "requires_action") {
  // 需要 3D Secure 认证
  console.log("需要额外认证:", paymentIntent.next_action);
}

优点:

  • 完全控制支付流程
  • 适合复杂业务逻辑
  • 可以实现订阅、分账等高级功能

缺点:

  • 开发复杂度最高
  • 需要处理更多边界情况

三种方式对比

特性CheckoutElementsPayment Intents API
开发难度简单中等复杂
UI 定制有限灵活完全自定义
页面跳转需要不需要不需要
适用场景快速上线定制体验复杂业务
PCI 合规Stripe 负责Stripe 负责Stripe 负责

实现支付流程

本章将从零开始,手把手实现一个完整的支付功能。我们使用 Stripe Elements 方式,前端用原生 HTML/JavaScript,后端用 Node.js。

注册 Stripe 账户

访问 Stripe 官网 注册账户:

  1. 点击右上角 "Sign in" → "Sign up"
  2. 填写邮箱、姓名,创建密码
  3. 验证邮箱
  4. 登录后会自动进入测试模式(Test mode)

注册完成后,在 Dashboard 左侧菜单找到 "Developers" → "API keys",会看到两种密钥:

  • Publishable key(公开密钥):以 pk_test_ 开头,用于前端
  • Secret key(密钥):以 sk_test_ 开头,用于后端

根据 Stripe API Keys 文档,测试模式的密钥不会产生真实扣款。

理解 Publishable Key 和 Secret Key

Stripe 使用两种密钥实现安全隔离:

特性Publishable Key (公钥)Secret Key (私钥)
格式pk_test_xxx / pk_live_xxxsk_test_xxx / sk_live_xxx
使用位置前端(浏览器)后端(服务器)
是否可公开✅ 可以公开❌ 绝对保密
权限级别只读,创建 token完全权限(创建、查询、修改、删除)

为什么要分开?

根据 Stripe API Keys 文档:

  1. 安全隔离: 前端代码会被用户看到,如果使用私钥,用户可以直接调用 API 执行任意操作(退款、查询所有客户等)
  2. 最小权限原则: 公钥只有创建 token 的权限,无法执行敏感操作
  3. 防止滥用: 即使公钥泄露,也只能收集支付信息,无法直接扣款

使用场景:

// 前端(使用公钥):收集支付信息
const stripe = Stripe('pk_test_xxx');  // 公钥可以直接写在前端
const cardElement = elements.create('card');
await stripe.confirmCardPayment(clientSecret, {
  payment_method: { card: cardElement }
});

// 后端(使用私钥):创建支付、退款等操作
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);  // 私钥必须用环境变量
const paymentIntent = await stripe.paymentIntents.create({ amount: 2000 });

这种设计确保了即使前端代码完全暴露,也不会危及账户安全。

项目初始化

创建项目目录并初始化:

# 创建项目目录
mkdir stripe-payment-demo
cd stripe-payment-demo

# 初始化 npm 项目
npm init -y

# 安装依赖
npm install stripe express dotenv

创建项目结构:

stripe-payment-demo/
├── .env              # 环境变量(存放 API 密钥)
├── server.js         # 后端服务器
├── public/
│   ├── index.html    # 前端页面
│   └── checkout.js   # 前端逻辑
└── package.json

配置环境变量

创建 .env 文件,存放 Stripe 密钥:

# .env
STRIPE_SECRET_KEY=sk_test_你的密钥
STRIPE_PUBLISHABLE_KEY=pk_test_你的公开密钥

重要:不要将 .env 文件提交到 Git。创建 .gitignore:

# .gitignore
node_modules/
.env

后端实现

创建 server.js,实现两个 API:

// server.js
require("dotenv").config();
const express = require("express");
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);

const app = express();

// 中间件
app.use(express.json());
app.use(express.static("public"));

// API 1: 创建 PaymentIntent
app.post("/create-payment-intent", async (req, res) => {
  try {
    const { amount } = req.body;

    // 创建 PaymentIntent
    const paymentIntent = await stripe.paymentIntents.create({
      amount: amount, // 金额(分为单位)
      currency: "usd",
      automatic_payment_methods: {
        enabled: true, // 自动启用支持的支付方式
      },
    });

    // 返回 clientSecret 给前端
    res.json({ clientSecret: paymentIntent.client_secret });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// API 2: 获取公开密钥
app.get("/config", (req, res) => {
  res.json({ publishableKey: process.env.STRIPE_PUBLISHABLE_KEY });
});

// 启动服务器
const PORT = 3000;
app.listen(PORT, () => {
  console.log(`服务器运行在 http://localhost:${PORT}`);
});

根据 Payment Intents API 文档,client_secret 用于前端确认支付。

前端实现

创建 public/index.html:

<!-- public/index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Stripe 支付示例</title>
    <script src="https://js.stripe.com/v3/"></script>
    <style>
      body {
        font-family: Arial, sans-serif;
        max-width: 500px;
        margin: 50px auto;
        padding: 20px;
      }
      #card-element {
        border: 1px solid #ccc;
        padding: 10px;
        border-radius: 4px;
        margin-bottom: 20px;
      }
      button {
        background: #5469d4;
        color: white;
        padding: 10px 20px;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        font-size: 16px;
        width: 100%;
      }
      button:disabled {
        opacity: 0.6;
        cursor: not-allowed;
      }
      #message {
        margin-top: 20px;
        padding: 10px;
        border-radius: 4px;
      }
      .success {
        background: #d4edda;
        color: #155724;
      }
      .error {
        background: #f8d7da;
        color: #721c24;
      }
    </style>
  </head>
  <body>
    <h1>Stripe 支付示例</h1>

    <form id="payment-form">
      <label for="amount">金额(美元):</label>
      <input type="number" id="amount" value="10" min="1" style="width: 100%; padding: 8px; margin-bottom: 20px;" />

      <label>信用卡信息:</label>
      <div id="card-element"></div>

      <button id="submit-button">支付</button>
    </form>

    <div id="message"></div>

    <script src="checkout.js"></script>
  </body>
</html>

创建 public/checkout.js:

// public/checkout.js
let stripe;
let elements;
let cardElement;

// 初始化 Stripe
async function initialize() {
  // 从后端获取公开密钥
  const response = await fetch("/config");
  const { publishableKey } = await response.json();

  stripe = Stripe(publishableKey);
  elements = stripe.elements();

  // 创建卡片输入组件
  cardElement = elements.create("card");
  cardElement.mount("#card-element");
}

// 处理表单提交
document.getElementById("payment-form").addEventListener("submit", async (e) => {
  e.preventDefault();

  const submitButton = document.getElementById("submit-button");
  const messageDiv = document.getElementById("message");

  // 禁用按钮
  submitButton.disabled = true;
  messageDiv.textContent = "处理中...";
  messageDiv.className = "";

  try {
    // 获取金额(转换为分)
    const amount = document.getElementById("amount").value * 100;

    // 步骤 1: 后端创建 PaymentIntent
    const response = await fetch("/create-payment-intent", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ amount }),
    });
    const { clientSecret } = await response.json();

    // 步骤 2: 前端确认支付
    const result = await stripe.confirmCardPayment(clientSecret, {
      payment_method: {
        card: cardElement,
      },
    });

    // 步骤 3: 处理结果
    if (result.error) {
      // 支付失败
      messageDiv.textContent = "支付失败: " + result.error.message;
      messageDiv.className = "error";
    } else if (result.paymentIntent.status === "succeeded") {
      // 支付成功
      messageDiv.textContent = "支付成功!";
      messageDiv.className = "success";
      // 清空表单
      cardElement.clear();
    }
  } catch (error) {
    messageDiv.textContent = "错误: " + error.message;
    messageDiv.className = "error";
  } finally {
    // 恢复按钮
    submitButton.disabled = false;
  }
});

// 页面加载时初始化
initialize();

根据 Stripe.js 文档,confirmCardPayment 方法会自动处理 3D Secure 认证。

运行项目

启动服务器:

node server.js

访问 http://localhost:3000,使用 Stripe 提供的测试卡号:

卡号说明
4242 4242 4242 4242支付成功
4000 0000 0000 0002卡被拒绝
4000 0025 0000 3155需要 3D Secure 认证

根据 Stripe 测试卡号文档,测试卡号的过期日期使用未来任意日期,CVC 使用任意 3 位数字。

验证支付结果

支付成功后,在 Stripe Dashboard 查看:

  1. 登录 Stripe Dashboard
  2. 左侧菜单点击 "Payments"
  3. 可以看到刚才的测试支付记录

完整支付流程图

1. 用户输入金额和卡号
   ↓
2. 前端调用后端 API: /create-payment-intent
   ↓
3. 后端创建 PaymentIntent,返回 clientSecret
   ↓
4. 前端使用 clientSecret 调用 stripe.confirmCardPayment()
   ↓
5. Stripe 处理支付(可能需要 3D Secure)
   ↓
6. 返回支付结果
   ↓
7. 前端显示成功/失败消息

参考链接:

Webhook 事件处理

Webhook 是 Stripe 主动通知你的服务器支付状态变化的机制。处理 Webhook 是生产环境的必要步骤。

为什么需要 Webhook

前端的支付确认可能不可靠:

  • 用户可能在支付成功后关闭浏览器
  • 网络中断导致前端无法收到结果
  • 异步支付方式(银行转账)需要等待数小时

根据 Stripe Webhooks 文档,Webhook 确保服务器能可靠地接收支付状态通知。

Webhook 工作原理

1. 支付状态变化(如支付成功)
   ↓
2. Stripe 向你的服务器发送 POST 请求
   ↓
3. 你的服务器验证请求签名
   ↓
4. 处理事件(如发货、更新订单状态)
   ↓
5. 返回 200 状态码给 Stripe

添加 Webhook 处理

在之前的 server.js 中,app.use(express.json()) 之前添加 Webhook 端点:

// Webhook 端点需要使用原始请求体,必须在 express.json() 之前
app.post("/webhook", express.raw({ type: "application/json" }), async (req, res) => {
  const sig = req.headers["stripe-signature"];
  const webhookSecret = process.env.WEBHOOK_SECRET;

  let event;

  try {
    // 验证 Webhook 签名
    event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
  } catch (err) {
    console.error("Webhook 签名验证失败:", err.message);
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // 处理不同的事件类型
  switch (event.type) {
    case "payment_intent.succeeded":
      const paymentIntent = event.data.object;
      console.log("支付成功:", paymentIntent.id);
      console.log("金额:", paymentIntent.amount / 100, paymentIntent.currency);

      // 这里执行业务逻辑:
      // - 更新订单状态
      // - 发送确认邮件
      // - 触发发货流程等
      break;

    case "payment_intent.payment_failed":
      const failedPayment = event.data.object;
      console.log("支付失败:", failedPayment.id);
      console.log("失败原因:", failedPayment.last_payment_error?.message);

      // 这里执行业务逻辑:
      // - 通知用户支付失败
      // - 记录失败日志
      break;

    case "charge.refunded":
      const refund = event.data.object;
      console.log("退款成功:", refund.id);

      // 这里执行业务逻辑:
      // - 更新订单为已退款
      // - 通知用户
      break;

    default:
      console.log(`未处理的事件类型: ${event.type}`);
  }

  // 返回 200 状态码告诉 Stripe 已收到
  res.json({ received: true });
});

重要:Webhook 端点必须在 express.json() 之前,因为需要原始请求体来验证签名。根据 Webhook 签名验证文档,验证签名可以防止伪造的 Webhook 请求。

获取 Webhook Secret

在 Stripe Dashboard 中配置 Webhook 并获取 Secret:

  1. 登录 Stripe Dashboard
  2. 点击 "Developers" → "Webhooks" → "Add endpoint"
  3. 填写 Endpoint URL: https://example.com/webhook
    • 必须填写一个 URL 才能继续,先填写占位符
    • 生产环境时再修改为真实 URL
  4. 选择要监听的事件:
    • 推荐选择: payment_intent.succeededpayment_intent.payment_failedcharge.refunded
    • 或选择 "Select all events"
  5. 点击 "Add endpoint"
  6. 进入新创建的 Webhook,点击 "Signing secret" 旁的 "Reveal" 按钮
  7. 复制显示的 whsec_xxx 格式的 secret

.env 文件中添加:

# .env
WEBHOOK_SECRET=whsec_xxx  # 从 Dashboard 复制的 Signing secret

本地测试 Webhook:

由于本地服务器没有公网 IP,使用 Stripe CLI 转发:

# 安装 Stripe CLI (macOS)
brew install stripe/stripe-cli/stripe

# 登录
stripe login

# 转发 Webhook 到本地
stripe listen --forward-to localhost:3000/webhook

命令行会显示一个临时的 webhook secret,复制到 .env 文件用于本地测试。

常见事件类型

事件类型说明使用场景
payment_intent.succeeded支付成功更新订单状态、发货
payment_intent.payment_failed支付失败通知用户、记录日志
charge.refunded退款成功更新订单、通知用户
customer.subscription.created订阅创建开通服务、发送欢迎邮件
customer.subscription.deleted订阅取消停止服务、通知用户
invoice.payment_succeeded订阅扣款成功续费成功、发送收据

完整事件列表请查看 Stripe Event Types 文档