better-auth Stripe 插件演进全解析:订阅生命周期回调、组织计费修复与安全加固
2026/9/10 19:59:28 网站建设 项目流程

better-auth Stripe 插件演进全解析:订阅生命周期回调、组织计费修复与安全加固

【免费下载链接】better-authThe most comprehensive authentication framework项目地址: https://gitcode.com/GitHub_Trending/be/better-auth

导读

本文以 packages/stripe/CHANGELOG.md 为核心骨架,系统梳理@better-auth/stripe从 1.6.0 到 1.7.3 的关键演进:订阅生命周期回调的语义统一、组织订阅与按席位计费的正确性修复、Checkout 会话参数的保护边界,以及元数据合并的安全加固。读完本文,你将掌握该插件各版本的行为差异、底层实现原理与升级迁移要点,并能在实际项目中准确使用onSubscriptionCancelgetCheckoutSessionParams等核心 API。

一、版本脉络与演进主线

@better-auth/stripe是 Better Auth 官方维护的 Stripe 计费集成插件(安装命令npm install @better-auth/stripe),其 CHANGELOG 记录了从 1.6.0 到 1.7.3 的完整变更。纵观这些变更,可以提炼出四条清晰的主线:

演进主线代表版本核心变更
订阅生命周期回调语义统一1.6.10 / 1.7.0所有回调统一接收stripeSubscription(原始 Stripe 对象)与"更新后"的订阅行;onSubscriptionCancelevent参数改为必填
组织订阅正确性修复1.6.17 / 1.6.21 / 1.6.24组织订阅操作不再作用到错误的组织;删除组织时校验全部订阅;钩子上下文正确透传
Checkout 与同步逻辑加固1.6.10 / 1.6.17插件内部管理的字段不再被用户覆盖;/subscription/success改为从 Checkout Session 同步;取消只删除目标订阅行
安全与健壮性1.6.3 / 1.6.17元数据合并防护原型污染;returnUrl校验trustedOrigins

下文将沿着这四条主线,结合 packages/stripe/src 下的源码逐一展开。

二、订阅生命周期回调体系:语义统一是 1.6.x–1.7.x 的主旋律

订阅生命周期回调是插件的核心扩展点,定义在 packages/stripe/src/types.ts 的SubscriptionOptions中。当前完整的回调族包括:

  • onSubscriptionComplete:Checkout 完成后触发,携带eventstripeSubscriptionsubscriptionplan
  • onSubscriptionCreatedcustomer.subscription.created事件触发(如从 Stripe Dashboard 直接创建订阅);
  • onSubscriptionUpdate:每次订阅更新事件触发;
  • onSubscriptionCancel:订阅进入"待取消"状态(cancel_at_period_end或计划中的cancel_at)时触发一次;
  • onSubscriptionDeleted:订阅删除事件触发;
  • onTrialStart/onTrialEnd/onTrialExpired:免费试用三个阶段的回调(定义在StripePlan.freeTrial内)。

2.1 1.6.10:回调参数形态统一

CHANGELOG 中 1.6.10 的三条变更直接塑造了今天回调的签名:

  1. onSubscriptionUpdate新增stripeSubscription——回调现在能拿到"原始 Stripe 对象",用于读取本地订阅行未持久化的字段(如cancellation_detailslatest_invoice等)。
  2. onSubscriptionCancel改为接收更新后的订阅行,而不再是更新前的快照。
  3. onSubscriptionDeletedonTrialEndonTrialExpired同样改为接收更新后的订阅行,与其他生命周期回调保持一致。

从 packages/stripe/src/hooks.ts 的实现可以看到这套语义的落地:在onSubscriptionUpdated中,插件先用ctx.context.adapter.update将最新状态写入数据库,得到subscriptionUpdated后再触发onSubscriptionCancel(仅当"新进入待取消状态"时)、onSubscriptionUpdateonTrialEnd/onTrialExpired(仅当状态从trialing迁移时)。这意味着你的回调读到的订阅行永远与数据库一致,无需自行回查。

2.2 1.7.0:onSubscriptionCancelevent参数变为必填(破坏性变更)

1.7.0 的 Minor Changes 明确要求:onSubscriptionCancel回调的event参数现在是必填的,与其他订阅生命周期回调保持一致。升级到 1.7.0 时,你需要:

subscription: { enabled: true, plans, onSubscriptionCancel: async ({ event, subscription, stripeSubscription }) => { // 1.7.0 之前 event 可能为 undefined,需要 !event 守卫; // 现在 event 必定存在,直接使用即可 await trackCancellation(event, subscription); }, },

即:声明event为必选参数,并删除原先围绕它的undefined守卫代码。

2.3 Webhook 驱动:回调与数据库同步的底层链路

这些回调全部由 Stripe Webhook 驱动。在 packages/stripe/src/routes.ts 中,stripeWebhook端点将事件分发给 packages/stripe/src/hooks.ts 中的四个处理器:onCheckoutSessionCompletedonSubscriptionCreatedonSubscriptionUpdatedonSubscriptionDeleted

onSubscriptionCreated为例(hooks.ts),其处理顺序为:解析事件对象 → 通过subscriptionMetadata查找本地订阅 ID → 若已存在则跳过(保证幂等)→ 通过stripeCustomerId反查 user 或 organization → 用resolvePlanItem匹配计划 → 写入本地subscription表 → 触发onSubscriptionCreated。其中resolvePlanItem(utils.ts)会同时匹配priceIdannualDiscountPriceId及 lookup key,这是插件能把 Stripe 订阅"翻译"成本地计划名的关键。

本地订阅表的字段定义在 packages/stripe/src/schema.ts:planreferenceIdstripeCustomerIdstripeSubscriptionIdstatus(默认incomplete)、periodStart/periodEndtrialStart/trialEndcancelAtPeriodEndcancelAt/canceledAt/endedAtseatsbillingIntervalstripeScheduleId。其中stripeScheduleId用于记录"计划变更排期"(scheduleAtPeriodEnd场景),Webhook 更新时会同步写入。

三、组织订阅与按席位计费:1.6.17 / 1.6.21 / 1.6.24 的正确性修复

组织订阅是插件最复杂的场景,CHANGELOG 中多条修复都集中于此。

3.1 1.6.21:订阅操作不再作用于错误的组织

1.6.21 修复了"取消、升级、恢复订阅以及计费门户可能作用于错误组织"的问题。其根源在于 packages/stripe/src/middleware.ts 的referenceMiddleware:当customerType === "organization"时,referenceId取请求体/查询参数中的显式值,否则回退到session.activeOrganizationId;随后必须通过authorizeReference校验该referenceId确实属于当前用户,未配置authorizeReference时直接抛出ORGANIZATION_SUBSCRIPTION_NOT_ENABLED/AUTHORIZE_REFERENCE_REQUIRED错误。

配套的修复体现在cancelSubscriptionrestoreSubscriptionupgradeSubscription三个端点中:当请求携带subscriptionId时,插件会先按stripeSubscriptionId查出订阅行,并校验subscription.referenceId === referenceId,不匹配即视为未找到(routes.ts),从源头杜绝跨组织操作。

3.2 1.6.17:取消/恢复只作用于目标订阅,组织删除校验全量订阅

1.6.17 的修复条目非常密集,核心有三点:

  1. 取消订阅只删除目标行:此前取消操作会删除所有共享同一referenceId的订阅行;现在遇到过期数据(resource_missing)时,仅删除subscription.id对应的那一行(routes.ts),不再误伤同一引用下的其他订阅。
  2. 恢复订阅精确瞄准restoreSubscription通过retrieveStripeSubscription读取stripeSubscriptionId指向的具体订阅,并据其cancel_at/cancel_at_period_end状态精确清理,而不是"客户的第一份活跃订阅"。
  3. 组织删除检查全量订阅:在 packages/stripe/src/index.ts 的beforeDeleteStripeOrg中,插件用for await自动分页遍历该组织客户的全部订阅(limit: 100),只要存在非canceled/incomplete/incomplete_expired状态的订阅就抛出ORGANIZATION_HAS_ACTIVE_SUBSCRIPTION,阻止删除。

3.3 1.6.24:组织删除钩子的上下文透传

1.6.24 修复了 organization 插件beforeDeleteOrganization/afterDeleteOrganization钩子签名不一致的问题:Stripe 插件的beforeDeleteOrganization包装层现在会把 endpoint context 作为第二个参数转发给用户提供的钩子,与文档及databaseHooks的既有模式对齐(index.ts)。如果你自定义过组织删除钩子并依赖 ctx 参数,此版本后行为才正确。

3.4 按席位计费(seat-based billing)

组织订阅还支持按席位计费:StripePlan.seatPriceId配合prorationBehavior(默认create_prorations)使用(types.ts)。成员变动时,插件通过afterAddMemberafterRemoveMemberafterAcceptInvitation三个组织钩子触发syncSeatsAfterMemberChange(index.ts):统计member表中的成员数,匹配seatPriceId,再调用client.subscriptions.update同步 quantity 并更新本地seats字段。注意:使用seatPriceId必须同时启用organization: { enabled: true },否则插件会在初始化时输出错误日志(index.ts)。

四、Checkout 会话参数:内部字段保护与免费试用修复(1.6.10)

getCheckoutSessionParams允许开发者为 Checkout Session 追加自定义参数(types.ts)。1.6.10 明确划定了它的权限边界:success_urlcancel_urlmodecustomercustomer_emailclient_reference_idline_items这七个字段由插件内部管理,无法被覆盖

在 routes.ts 中可以看到实现:插件先从params?.params中解构剥离这七个字段(_mode_customer等),其余参数原样透传,再强制写入插件计算的值。这保证了 Webhook 对账(subscriptionMetadata中的referenceId/subscriptionId)和计费链路不被破坏。此外locale现在优先取请求体的值,其次才是getCheckoutSessionParams

1.6.10 还修复了免费试用的两个隐藏缺陷:返回自定义subscription_data不再遮蔽计划的免费试用期(trial_period_days),也不会在customer.subscription.createdWebhook 触发时产生重复的本地订阅行。注意免费试用本身有"每个 reference 终身一次"的约束——插件通过检查该referenceId下所有历史订阅是否曾出现过trialStart/trialEndtrialing状态来判断(routes.ts),防止用户通过切换计划反复薅试用。

五、安全加固:元数据合并防原型污染(1.6.3 / 1.7.0-beta.1)

1.6.3 与 1.7.0-beta.1 记录了同一项安全修复:插件此前通过defu深度合并ctx.body.metadata,当攻击者构造__proto__等键时存在原型污染风险。由于 Stripe metadata 本质是扁平的Record<string, string>,深度合并本无必要,因此修复后:

  • 合并ctx.body.metadata时忽略__proto__constructorprototype三个危险键,用户可控面不再依赖defu
  • 仅保留开发者提供的CustomerCreateParams深度合并场景使用经过补丁的defu范围(见 index.ts 中defu合并getCustomerCreateParams的部分)。

1.6.17 还同步加固了 URL 校验:/subscription/upgradereturnUrl现在与/subscription/cancel、计费门户一致,通过originCheck中间件校验trustedOrigins(routes.ts),防止开放重定向。

六、客户创建与复用:1.6.17 的邮箱复用规则

当开启createCustomerOnSignUp时,插件在用户注册后自动创建 Stripe Customer(index.ts)。1.6.17 明确了"按邮箱复用客户"的边界条件:

  • 仅当邮箱已验证时才按邮箱复用现有客户;未验证邮箱的注册会创建全新客户;
  • 即使邮箱匹配,若该客户已通过 metadata(customerMetadata中的userId)关联到其他用户,也不会复用,防止客户归属错乱。

复用逻辑优先使用 Stripe Search API(customers.search),失败时回退到customers.list分页遍历(搜索 API 在部分区域不可用)。客户查找与创建同时会调用onCustomerCreate回调,并同步写入用户的stripeCustomerId字段。

七、其他值得关注的变化

  • 1.6.17:/subscription/success同步来源变更——现在从 Checkout Session 同步订阅(读取checkoutSession.subscription再 retrieve),而不是"客户的第一份活跃订阅",避免在多订阅场景下同步错对象。
  • 1.6.0:插件版本字段与年付价格修复——插件接口新增可选version字段,所有内置插件对外暴露版本;同时修复了年度订阅在订阅列表中的priceId返回错误的问题。插件源码通过 packages/stripe/src/version.ts 的PACKAGE_VERSION注入该字段(index.ts)。
  • 常规依赖同步:CHANGELOG 中大量条目为better-auth@better-auth/core的版本跟随(Patch Changes),升级插件时建议与核心包保持同版本基线,避免类型不匹配。

八、升级与迁移清单

  1. 升级到 1.7.0:将onSubscriptionCancelevent参数改为必填,删除undefined守卫。
  2. 升级到 1.6.10+:确认onSubscriptionUpdate/onSubscriptionCancel/onSubscriptionDeleted/onTrialEnd/onTrialExpired回调读取的是"更新后"的订阅行与原始stripeSubscription,如有基于旧快照的假设需调整。
  3. 升级到 1.6.21+:检查组织订阅操作是否依赖"错误组织"的旧行为;确认已配置authorizeReference(组织订阅的硬性要求,见 middleware.ts)。
  4. 升级到 1.6.3+(安全相关):确认没有依赖__proto__/constructor/prototype键作为业务 metadata 的代码。
  5. 升级到 1.6.17+getCheckoutSessionParams中若曾自定义success_url等七个保留字段,需改为在请求体的successUrl/cancelUrl或插件选项层配置。

结语

从 1.6.0 到 1.7.3,@better-auth/stripe的演进始终围绕三条原则:回调语义一致、组织/用户订阅边界清晰、插件内部状态不可被外部参数破坏。CHANGELOG 中的每一条修复都能在 packages/stripe/src 的源码(尤其 routes.ts、hooks.ts、middleware.ts)中找到对应实现,配合 packages/stripe/test 下的webhook.test.tssubscription.test.tsstripe-organization.test.tsseat-based-billing.test.ts等测试,你可以完整验证这些行为。对正在接入或维护 Stripe 计费的团队而言,理解这些演进细节,能帮助你写出更健壮、更安全、可平滑升级的订阅业务代码。

【免费下载链接】better-authThe most comprehensive authentication framework项目地址: https://gitcode.com/GitHub_Trending/be/better-auth

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询