如何为 Chrome extension 添加按月订阅付款
结论
Chrome extension 要实现按月订阅,建议采用“第三方支付平台 + 自有后端 + 订阅权限校验”的方案:
- 在 Stripe Billing、Paddle 等平台创建月度订阅,并使用平台托管的结账页面。
- 扩展负责用户登录、打开付款页面和查询订阅状态。
- 后端负责创建 Checkout Session、接收 Webhook,以及保存用户的订阅权限。
- 是否开放高级功能,以后端返回的权限状态为准。
不要在扩展中保存支付密钥或处理银行卡信息,也不要再使用已经停止服务的 Chrome Web Store Payments。最终采用哪种结账方式,还要根据发布时有效的 Chrome Web Store 政策,以及支付平台支持的地区来决定。
为什么需要后端
Chrome extension 的代码会下载到用户设备,无法安全保存 STRIPE_SECRET_KEY 等敏感凭据。压缩或混淆代码也解决不了这个问题,攻击者仍可能提取密钥。
前端显示“付款成功”同样不能证明订阅仍然有效。用户可能取消付款、申请退款或发生拒付,订阅也可能因扣款失败进入异常状态。后端需要通过支付平台的 Webhook 同步订阅状态,再向扩展提供查询结果。
常见的数据流如下:
Chrome extension
├── 用户登录
├── 请求后端创建 Checkout Session
└── 查询当前 entitlement
│
▼
自有后端
├── 调用支付平台 API
├── 接收并验证 Webhook
└── 保存订阅和权限状态
│
▼
Stripe / Paddle
这里的 entitlement 表示用户是否有权使用付费功能。与检查某个页面是否显示“付款成功”相比,查询 entitlement 更可靠。
实现步骤
1. 建立用户身份系统
首先要解决“付款记录属于哪个扩展用户”的问题。可以使用:
- 自有邮箱登录;
- Google OAuth;
- Firebase Authentication、Auth0、Supabase Auth 等身份服务;
- 通过
chrome.identity获取授权身份,再交给后端验证。
不要把浏览器安装 ID 作为唯一的订阅账号。用户重装扩展、清理数据或更换电脑后,安装 ID 可能会变化。订阅应该绑定到可以恢复的用户账号。
2. 在支付平台创建月度价格
以 Stripe 为例,需要在 Dashboard 中创建:
- 一个 Product,例如
Extension Pro; - 一个按月循环收费的 Price;
- 对应的
price_...标识。
金额、币种、税务处理和试用期应该在支付平台中配置,不要允许扩展传入任意价格。后端只接受预先允许的 Price ID,防止用户通过篡改请求修改金额。
3. 后端创建结账会话
下面的示例使用 Node.js、Express 和 Stripe。实际项目还要加入登录鉴权、参数校验、限流和错误处理。
import express from "express";
import Stripe from "stripe";
const app = express();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
app.use(express.json());
app.post("/api/billing/checkout", requireUser, async (req, res) => {
const user = req.user;
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [
{
price: process.env.STRIPE_MONTHLY_PRICE_ID,
quantity: 1
}
],
client_reference_id: user.id,
customer_email: user.email,
success_url: "https://example.com/billing/success?session_id={CHECKOUT_SESSION_ID}",
cancel_url: "https://example.com/billing/cancel",
metadata: {
userId: user.id
}
});
res.json({ url: session.url });
});
requireUser 代表项目自己的鉴权中间件。不要信任客户端直接提交的 userId,后端应该从已验证的登录令牌中获取当前用户。
4. 从扩展打开托管付款页面
扩展向后端请求 Checkout URL,然后在普通浏览器标签页中打开:
async function startSubscription() {
const token = await getAccessToken();
const response = await fetch(
"https://api.example.com/api/billing/checkout",
{
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
}
}
);
if (!response.ok) {
throw new Error("Unable to create checkout session");
}
const { url } = await response.json();
if (!url) {
throw new Error("Checkout URL is missing");
}
await chrome.tabs.create({ url });
}
如果扩展使用 Manifest V3,需要在 manifest.json 中配置访问后端所需的权限:
{
"manifest_version": 3,
"name": "Example Extension",
"version": "1.0.0",
"permissions": ["storage"],
"host_permissions": [
"https://api.example.com/*"
]
}
是否需要 tabs 权限,要看扩展具体使用了哪个 Chrome API。不要为了方便而申请与功能无关的广泛权限。
5. 使用 Webhook 同步订阅状态
支付平台会通过 Webhook 通知后端订阅创建、续费成功、取消、退款和支付失败等事件。后端必须验证 Webhook 签名。
Stripe Webhook 路由需要读取原始请求体,所以通常要在全局 express.json() 之前注册:
app.post(
"/api/billing/webhook",
express.raw({ type: "application/json" }),
async (req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
req.headers["stripe-signature"],
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (error) {
return res.status(400).send("Invalid webhook signature");
}
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object;
await linkSubscriptionToUser({
userId: session.metadata?.userId,
customerId: session.customer,
subscriptionId: session.subscription
});
break;
}
case "customer.subscription.updated":
case "customer.subscription.deleted": {
const subscription = event.data.object;
await updateSubscriptionStatus({
subscriptionId: subscription.id,
status: subscription.status,
currentPeriodEnd: getCurrentPeriodEnd(subscription)
});
break;
}
case "invoice.paid":
case "invoice.payment_failed":
await processInvoiceEvent(event);
break;
}
res.sendStatus(200);
}
);
支付平台可能调整事件结构,开发时应参考当前 SDK 的类型定义和官方文档。例如,订阅周期字段在不同 API 版本中的位置可能不同,示例里的 getCurrentPeriodEnd() 需要按照实际响应来实现。
Webhook 处理还要做到:
- 使用
event.id进行幂等处理; - 能够处理事件的重复投递;
- 不假设事件会按顺序到达;
- 先快速返回成功,再异步处理耗时任务;
- 保存必要的事件日志,以便排查账单问题。
6. 提供权限查询接口
扩展不应该直接调用 Stripe 查询用户订阅,而应该查询自己的后端:
app.get("/api/entitlements", requireUser, async (req, res) => {
const entitlement = await findEntitlement(req.user.id);
res.json({
plan: entitlement?.plan ?? "free",
active: entitlement?.active ?? false,
validUntil: entitlement?.validUntil ?? null
});
});
扩展根据返回结果启用或禁用付费功能:
async function refreshEntitlements() {
const token = await getAccessToken();
const response = await fetch(
"https://api.example.com/api/entitlements",
{
headers: {
"Authorization": `Bearer ${token}`
}
}
);
if (!response.ok) {
throw new Error("Unable to load subscription status");
}
const entitlement = await response.json();
await chrome.storage.local.set({
entitlement,
entitlementCheckedAt: Date.now()
});
return entitlement;
}
本地缓存可以加快启动速度,但不能作为永久可信的数据源。如果某项功能成本较高或容易被滥用,例如云端处理或 API 调用额度,后端应该在每次请求时重新验证权限。
7. 提供订阅管理入口
用户还需要修改付款方式、下载发票或取消订阅。通常可以直接使用支付平台提供的 Customer Portal:
app.post("/api/billing/portal", requireUser, async (req, res) => {
const customerId = await findStripeCustomerId(req.user.id);
if (!customerId) {
return res.status(404).json({ error: "Billing customer not found" });
}
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: "https://example.com/account"
});
res.json({ url: session.url });
});
扩展拿到 URL 后,同样通过 chrome.tabs.create() 打开。
订阅状态如何判断
不能只看订阅记录是否存在,至少要区分以下状态:
active:通常可以使用付费功能;trialing:是否可用由试用策略决定;past_due:可以设置一段较短的宽限期;unpaid:通常应该暂停付费权限;canceled:需要判断订阅是立即失效,还是在当前计费周期结束后失效;incomplete:首次付款尚未完成,不应该直接开通权限。
各支付平台的状态名称和具体含义可能不同。建议在数据库中保留原始支付状态,同时计算应用自己的权限字段,例如:
{
"userId": "user_123",
"plan": "pro",
"billingStatus": "active",
"entitled": true,
"validUntil": "2026-09-29T00:00:00Z"
}
这样,业务代码只需要判断 entitled,支付状态的转换规则统一由后端处理。
支付平台如何选择
Stripe Billing
Stripe Billing 适合已经有后端,并且希望自己控制结账流程和用户数据的团队。它的 API、Webhook 和订阅管理功能比较完整,但商户通常要承担更多税务、退款和合规工作。
Paddle 等 Merchant of Record 平台
在支持的地区,这类平台通常可以作为记录商户,代为处理部分销售税、VAT 和账单事务。相应的代价可能包括更高的费用、更严格的审核,以及对结账流程的限制。
选择平台前,需要确认:
- 是否支持商户注册地和目标销售地区;
- 是否支持需要的币种和付款方式;
- 平台属于支付处理商还是 Merchant of Record;
- 税务、退款和拒付分别由谁负责;
- 是否允许销售 Chrome extension 的数字服务;
- 提现周期、交易费用和订阅管理能力是否合适。
这些条件会随着地区和平台政策变化,上线前需要查看最新条款。
重要注意事项
不要把密钥放进扩展
下面这些内容都不应该出现在扩展源码、manifest.json 或 chrome.storage 中:
STRIPE_SECRET_KEY
STRIPE_WEBHOOK_SECRET
数据库管理员密钥
服务账号私钥
扩展中只能使用可以公开的标识,例如受限的 publishable key。如果使用托管 Checkout,通常连 publishable key 也不必放进扩展。
不要仅依赖成功跳转
success_url 的作用是改善用户体验,不能作为开通权限的凭据。是否开通权限,应该以经过签名验证的 Webhook,以及后端从支付平台确认的订阅状态为准。
考虑离线使用
如果扩展需要离线工作,可以缓存最近一次有效权限,并设置有限的离线宽限期。例如,在最后一次成功校验后的 24 小时内继续允许使用。宽限期越长,用户在权限被篡改或退款后继续使用的风险就越高。
明确取消和退款规则
购买前应该清楚说明:
- 自动续费周期;
- 如何取消;
- 取消后何时失效;
- 是否提供试用;
- 退款条件;
- 数据如何保存和删除。
还要提供隐私政策、服务条款,以及用户可以联系到的客户支持渠道。
遵守商店和支付政策
Chrome Web Store Payments 已经不适合用于新的订阅方案。即使使用外部支付服务,也要确保付款入口、功能描述、用户数据处理方式和远程代码行为符合发布时有效的 Chrome Web Store Developer Program Policies。
如果扩展还会发布到其他浏览器商店,需要分别检查各商店对外部支付和数字订阅的规定,不能假设所有商店都采用相同规则。
推荐的最小可用方案
对于准备首次上线的独立扩展,可以采用下面这套组合:
- Google OAuth 或邮箱登录;
- Stripe Checkout 月度订阅;
- 一个小型后端服务;
- 通过 Webhook 同步订阅;
- 在数据库中保存用户与订阅的映射关系;
- 由
/api/entitlements返回付费权限; - 使用 Customer Portal 管理取消订阅和付款方式。
这套方案的实现成本相对可控,以后也可以继续增加年付、优惠码、试用期、团队套餐和按量计费。
备注:内容仅供参考。
