PAYATHON 2026

是否有支持 SaaS 计费、注册和用户身份验证的服务?

支付阿杰

结论

有这类服务,但对 Windows 桌面客户端来说,很少有产品能包办注册、身份验证、订阅计费和权限控制。

更稳妥的方案是组合两类成熟服务:

  • 身份服务:Auth0、Microsoft Entra External ID、Amazon Cognito、Firebase Authentication、Clerk 等。
  • 计费服务:Stripe Billing、Paddle、Chargebee 等。

应用后端负责关联身份与计费数据,再向客户端返回用户当前可以使用的功能。密码存储、邮箱验证、支付信息、续费、账单和发票等复杂工作,仍然交给第三方处理。

如果想减少集成工作,也可以考虑 Outseta、Frontegg、Keygen 这类更接近“一体化”的产品。选择前,需要确认它们是否支持桌面应用登录、目标支付地区、税务处理、所需订阅模型和细粒度权限。相关能力和商业条款可能变化,不能只看产品是否标注“支持 SaaS”。

推荐的系统结构

整个流程可以设计成:

Windows 客户端
    │
    ├── 登录 ──────────> 身份服务(OIDC / OAuth 2.0)
    │                       │
    │                       └── 返回 access token 和稳定的 user ID
    │
    ├── 购买或管理订阅 ──> SaaS 后端 ──> 计费服务
    │
    └── 查询可用功能 ───> SaaS 后端 ──> 权限数据库
                                      ↑
                              计费 Webhook 更新

身份服务生成的用户标识通常位于令牌的 sub claim 中。后端以此为主键,将用户关联到计费平台中的 customer_id,并根据订阅状态生成相应权限。

例如:

{
  "identity_subject": "auth0|abc123",
  "billing_customer_id": "cus_xxxxxxxxx",
  "plan": "pro",
  "entitlements": [
    "project.create",
    "export.pdf",
    "sync.cloud"
  ]
}

不建议另外向客户端发放永久有效的“应用密钥”作为登录凭据。稳定的用户 ID 可以用来关联数据,身份验证则应依赖短期 access token。

实现步骤

1. 注册和登录

用户在 Windows 应用中输入邮箱后,可以选择以下方式之一:

  • 邮箱验证码或 magic link。
  • OAuth 2.0 Device Authorization Grant。
  • 通过系统浏览器完成 Authorization Code Flow with PKCE。

桌面应用属于 public client,无法安全保存 client_secret。不要把 OAuth client secret、支付密钥或后台管理 API key 写进安装包。

如果通过系统浏览器登录,身份服务完成验证后会返回类似下面的令牌信息:

{
  "access_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "id_token": "eyJ..."
}

客户端随后使用 access_token 调用自己的后端:

using System.Net.Http.Headers;
using System.Text.Json;

var http = new HttpClient
{
    BaseAddress = new Uri("https://api.example.com")
};

http.DefaultRequestHeaders.Authorization =
    new AuthenticationHeaderValue("Bearer", accessToken);

var response = await http.GetAsync("/v1/me/entitlements");
response.EnsureSuccessStatusCode();

var json = await response.Content.ReadAsStringAsync();
var entitlements = JsonSerializer.Deserialize<EntitlementResponse>(json);

public sealed class EntitlementResponse
{
    public string Plan { get; set; } = "";
    public string[] Entitlements { get; set; } = [];
}

这里的 accessToken 必须来自经过验证的 OIDC/OAuth 流程,不能由应用自行拼装。

2. 创建计费账户

用户首次购买服务时,后端先从已验证的令牌中取得 sub,然后创建或查找计费平台中的 customer。

下面使用 Stripe 的概念举例,具体 SDK 接口应以当前使用版本的官方文档为准:

const customer = await stripe.customers.create({
  email: verifiedEmail,
  metadata: {
    identity_subject: authenticatedUser.sub
  }
});

await users.update(authenticatedUser.sub, {
  billingCustomerId: customer.id
});

这项关联必须保存在服务端。不能根据客户端提交的邮箱判断支付账户归属,因为邮箱可能被修改,也可能存在大小写、别名或重复注册等问题。

3. 添加服务或订阅

用户选择套餐或附加服务后,由后端创建结账会话,客户端只负责打开服务端返回的结账地址:

app.post("/v1/billing/checkout", requireUser, async (req, res) => {
  const user = await users.findBySubject(req.user.sub);

  const session = await stripe.checkout.sessions.create({
    mode: "subscription",
    customer: user.billingCustomerId,
    line_items: [
      {
        price: process.env.STRIPE_PRO_PRICE_ID,
        quantity: 1
      }
    ],
    success_url: "https://example.com/billing/success",
    cancel_url: "https://example.com/billing/cancel"
  });

  res.json({ url: session.url });
});

price、金额和套餐类型都应由服务端决定,不能采用客户端传来的价格。

4. 根据 Webhook 更新权限

订阅状态变化后,计费平台会向后端发送 Webhook。后端验证签名,再更新用户权限。

app.post(
  "/webhooks/stripe",
  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 {
      return res.sendStatus(400);
    }

    if (
      event.type === "customer.subscription.created" ||
      event.type === "customer.subscription.updated" ||
      event.type === "customer.subscription.deleted"
    ) {
      const subscription = event.data.object;

      await synchronizeEntitlements({
        billingCustomerId: subscription.customer,
        subscriptionId: subscription.id,
        status: subscription.status,
        items: subscription.items.data
      });
    }

    res.sendStatus(200);
  }
);

Webhook 可能重复发送,也可能乱序到达,因此 synchronizeEntitlements 必须具备幂等性。不能因为收到一次“创建”事件就永久开放功能。更稳妥的方式是根据订阅 ID、状态和事件时间重新计算权限。

5. 为客户端提供权限接口

Windows 应用不应直接调用计费平台查询订阅,只需访问自己的权限接口:

GET /v1/me/entitlements
Authorization: Bearer eyJ...

响应可以是:

{
  "plan": "pro",
  "subscriptionStatus": "active",
  "entitlements": {
    "cloudSync": true,
    "maxProjects": 100,
    "pdfExport": true
  }
}

后端处理请求时,需要分别完成两项检查:

  1. 验证令牌签名、iss、aud、有效期等身份信息。
  2. 从服务端数据库读取当前订阅和权限。

只在客户端界面中隐藏按钮并不构成权限控制。涉及云端数据、付费资源或高成本操作时,服务端必须再次授权。

Freemium 模式如何处理

免费用户也应拥有正式账户,只是没有付费订阅:

{
  "plan": "free",
  "entitlements": {
    "cloudSync": false,
    "maxProjects": 3,
    "pdfExport": false
  }
}

“套餐”和“权限”最好分开建模。例如,可以把 pro 套餐映射到多个 entitlement,不要在代码各处直接判断:

if (user.plan === "pro") {
  // ...
}

更容易维护的写法是:

if (user.entitlements.includes("export.pdf")) {
  // ...
}

以后调整套餐内容、增加附加服务,或者为特定客户临时授权时,就不必修改客户端的业务逻辑。

选择服务时需要确认的事项

桌面客户端支持

确认身份平台支持 native application、PKCE、device flow 或 loopback redirect URI。只支持浏览器 Cookie 会话的会员系统,通常不适合 Windows 客户端。

支付和税务责任

Stripe Billing 主要提供支付与订阅基础设施。税务、退款、争议处理和销售主体责任,需要结合具体产品配置和经营地区确认。

Paddle 等 Merchant of Record 模式可能承担更多支付和税务事务,但各家的费率、国家覆盖范围、产品审核要求和退款政策不同,应以签约时的条款为准。

离线使用

如果客户端必须离线运行,仅靠在线查询权限还不够。后端可以签发短期授权文件,并通过非对称签名防止篡改:

{
  "sub": "user_123",
  "entitlements": ["export.pdf"],
  "exp": 1788200000
}

客户端只内置用于验签的公钥,私钥必须保留在服务端。离线授权还要明确宽限期、设备数量、撤销方式,以及客户端时钟被修改时的处理策略。

账户安全

至少需要做到以下几点:

  • 不在自己的数据库中保存明文密码。
  • 不在客户端保存支付平台 secret key。
  • 使用系统安全存储保存 refresh token。
  • 为 access token 设置较短的有效期,并实现安全续期。
  • 校验 JWT 的签名、issuer、audience 和过期时间。
  • 找回账户、修改邮箱或合并账户时重新验证身份。
  • 通过 Webhook 处理退款、取消、支付失败和订阅到期。

实际建议

如果目标是尽快上线,可以先采用“托管身份服务 + 托管结账页面 + 自有权限接口”的组合。通常只需要自行编写以下核心部分:

  • 身份用户与计费 customer 的映射。
  • Webhook 处理。
  • 套餐到 entitlement 的映射。
  • /me/entitlements 权限查询接口。

与寻找一个包办所有流程的平台相比,这套方案更容易控制,也能避免日后更换支付服务时迁移全部用户身份。对 Windows SaaS 而言,它在工作量、可靠性和扩展能力之间取得了更实际的平衡。

备注:内容仅供参考。