发布于 2026-01-05 1 阅读
0

为人类设计 API:对象 ID 条带

为人类设计 API:对象 ID

条纹

选择您的身份证类型

无论您经营何种类型的业务,您很可能都需要一个数据库来存储重要数据,例如客户信息或订单状态。但存储只是其中的一部分;您还需要一种快速检索数据的方法——这就是 ID 的作用所在。

ID 也称为主键,用于唯一标识表中的每一行。设计表时,您需要确保 ID 易于生成、唯一且易于理解。

在使用关系数据库时,最简单的 ID 处理方法是使用行 ID,它是一个整数。这样做的目的是,每当添加新行(例如,创建一个新客户)时,ID 就使用下一个顺序编号。这听起来不错,因为便于沟通(“56 号订单有问题,你能看一下吗?”),而且设置起来也毫不费力。然而,在实践中,这却是一个潜在的安全隐患。使用整数 ID 会使数据库极易受到枚举攻击,恶意攻击者可以轻而易举地猜到他们本不应该猜到的 ID,因为你的 ID 是顺序编号的。

例如,如果我注册了您的服务,发现我的用户 ID 是“42”,那么我就可以推测存在一个 ID 为“41”的用户。有了这一信息,我或许能够获取关于用户“41”的敏感数据,而这些数据我绝对不应该被允许获取,例如通过不安全的 API 端点/api/customers/:id/。如果 ID 是我无法猜测的,那么利用该端点进行攻击的难度就会大大增加。

使用整数ID也意味着您很可能泄露了一些关于贵公司的非常敏感的信息,例如公司规模和业绩(基于客户数量和订单量)。注册后发现自己只是第42位用户,我可能会对您声称的公司规模表示怀疑。

相反,你需要确保你的 ID 是唯一的,并且不可能被猜到。

更适合用作 ID 的是通用唯一标识符 (UUID )。它是由 32 位字母数字字符组成的组合(因此以字符串形式存储)。以下是一个示例:

4c4a82ed-a3e1-4c56-aa0a-26962ddd0425

它生成速度快,应用广泛,而且碰撞(新生成的 UUID 之前已经出现过或将来会出现的概率)极其罕见,因此被认为是唯一标识系统中对象的最佳方法之一,尤其适用于那些唯一性很重要的系统。

另一方面,以下是 Stripe 对象 ID:

pi_3LKQhvGUcADgqoEM3bh6pslE

有没有想过为什么 Stripe 会使用这种特定的格式?让我们深入了解一下 Stripe ID 的结构及其原因。

使其易于阅读

pi_3LKQhvGUcADgqoEM3bh6pslE
└─┘└──────────────────────┘
 └─ Prefix    └─ Randomly generated characters
Enter fullscreen mode Exit fullscreen mode

您可能已经注意到,所有 Stripe 对象的 ID 开头都有一个前缀。原因很简单:添加前缀使 ID更易于阅读。即使不了解 ID 的其他信息,我们也能立即通过这个前缀确认这是一个PaymentIntent对象pi_

当您通过 API 创建 PaymentIntent 时,实际上会创建或引用其他几个对象,包括 Customer( cus_)、PaymentMethod( pm_) 和 Charge( ch_)。通过前缀,您可以一目了然地区分所有这些不同的对象:

$pi = $stripe->paymentIntents->create([
  'amount' => 1000,
  'currency' => 'usd',
  'customer' => 'cus_MJA953cFzEuO1z',
  'payment_method' => 'pm_1LaXpKGUcADgqoEMl0Cx0Ygg',
]);
Enter fullscreen mode Exit fullscreen mode

这不仅对 Stripe 内部员工有帮助,对与 Stripe 集成的开发者也同样重要。例如,以下是我之前在被要求协助调试集成时看到的一段代码:

$pi = $stripe->paymentIntents->retrieve(
  $id,
  [],
  ['stripe_account' => 'cus_1KrJdMGUcADgqoEM']
);
Enter fullscreen mode Exit fullscreen mode

上面的代码片段试图从已连接的账户中检索 PaymentIntent ,但即使不看代码也能立即发现错误:cus_使用了客户 ID ( ) 而不是账户 ID ( acct_)。如果没有前缀,调试起来会困难得多;如果 Stripe 使用的是 UUID,那么我们就必须查找 ID(可能需要在 Stripe 控制面板中查找)才能确定它是什么类型的对象以及它是否有效。

在 Stripe,我们甚至开发了一个内部浏览器扩展程序,可以根据 ID 自动查找 Stripe 对象。因为我们可以通过前缀推断对象类型,所以只需三击 ID 即可自动打开相关的内部页面,大大简化了调试过程。

多态查找

说到推断对象类型,这在设计 API 时尤其重要,因为需要考虑到向后兼容性。

创建 PaymentIntent 时,您可以选择提供一个payment_method 参数来指定要使用的支付工具类型。您可能不知道,实际上您可以提供 Source(src_)或 Card(card_)ID,而不是 PaymentMethod(pm_)ID。PaymentMethod已取代Source 和 Card 成为 Stripe 中表示支付工具的标准方式,但出于向后兼容性的考虑,我们仍然需要支持这些旧对象。

$pi = $stripe->paymentIntents->create([
  'amount' => 1000,
  'currency' => 'usd',
  // This could be a PaymentMethod, Card or Source ID
  'payment_method' => 'card_1LaRQ7GUcADgqoEMV11wEUxU',
]);
Enter fullscreen mode Exit fullscreen mode

如果没有前缀,我们就无法知道 ID 代表什么类型的对象,也就不知道应该查询哪个表来获取对象数据。查询每个表来查找一个 ID 效率极低,因此我们需要一种更好的方法。一种方法是要求添加一个额外的“类型”参数:

$pi = $stripe->paymentIntents->create([
  'amount' => 1000,
  'currency' => 'usd',
  // Without prefixes, we'd have to supply a 'type'
  'payment_method' => [
    'type' => 'card',
    'id' => '1LaRQ7GUcADgqoEMV11wEUxU'
  ],
]);
Enter fullscreen mode Exit fullscreen mode

这样做虽然可行,但却使我们的 API 变得复杂,而且没有任何额外好处。它不再payment_method是一个简单的字符串,而是一个哈希值。此外,这里并没有任何无法合并成一个字符串的额外信息。每当使用 ID 时,您都需要知道它代表的对象类型,因此将这两种信息合并到一个数据源中,远比添加额外的“类型”参数要好得多。

通过前缀,我们可以立即推断支付工具是 PaymentMethod、Source 还是 Card,并知道要查询哪个表,尽管这些是完全不同类型的对象。

防止人为错误

前缀还有其他一些不太明显的优势,例如,您可以根据 ID 的前几个字符推断其类型,从而简化 ID 的使用。例如,在Stripe 的 Discord 服务器上,我们使用 Discord 的AutoMod功能自动标记并屏蔽包含 Stripe 实时密钥(以 `<string>` 开头)的消息sk_live_。泄露如此敏感的密钥可能会对您的业务造成严重后果,因此我们会采取措施,避免在我们控制的环境中发生这种情况。

通过让键以逗号开头sk_live_,编写正则表达式来过滤掉意外泄露的信息就变得非常简单:

使用 Discord 的 AutoMod 工具防止密钥泄露

这样我们就可以防止秘密的实时 API 密钥在我们的 Discord 中泄露,但允许以这种格式发布测试密钥sk_test_123(尽管你也绝对应该对这些密钥保密)。

说到 API 密钥,`--api`livetest`--api` 前缀是内置的保护层,可以防止您混淆两者。对于特别注重安全的用户,您还可以更进一步,设置检查,确保您仅在适当的环境中使用密钥:

if (preg_match("/sk_live/i", $_ENV["STRIPE_SECRET_API_KEY"])) {
  echo "Live key detected! Aborting!";
  return;
}

echo "Proceeding in test mode";
Enter fullscreen mode Exit fullscreen mode

Stripe 自 2012 年起就开始使用这种前缀技术,据我所知,我们是首家大规模应用该技术的公司。(如有错误,请在下方评论区留言!)2012 年之前,Stripe 的所有对象 ID 都更像传统的 UUID。如果您是 Stripe 的早期用户,您可能会注意到您的账户 ID 仍然保留着这种格式,没有前缀。

编辑: IETF比 Stripe 早几年就提出了URN规范。你在工作中用到 URN 格式了吗?请告诉我!

为人类设计 API

Stripe ID 的结构设计主要受我们希望为需要集成这些 ID 的开发者设计 API 的理念所驱动。计算机通常并不关心 ID 的具体格式,只要它是唯一的即可。但使用这些 ID 进行开发的开发者却非常在意,因此我们投入大量精力来提升 API 的开发者体验。

希望这篇文章能让你了解给 ID 添加前缀的好处。如果你想知道如何有效地实现这一点(并且恰好使用 Ruby),Chris Oliver 开发了一个gem,可以让你轻松地将此功能添加到系统中。

关于作者

保罗·阿斯杰斯

Paul Asjes是 Stripe 的开发者布道师,负责撰写代码、编写文档,并主持每月一次的开发者问答系列活动。工作之余,他喜欢酿造啤酒、制作南非牛肉干,以及在马里奥赛车游戏中输给儿子。

文章来源:https://dev.to/4thzoa/designing-apis-for- humans-object-ids-3o5a