为人类设计 API:对象 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
您可能已经注意到,所有 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',
]);
这不仅对 Stripe 内部员工有帮助,也对开发人员与 Stripe 集成有帮助。例如,这是我之前在调试集成时看到的一段代码片段:
$pi = $stripe->paymentIntents->retrieve(
$id,
[],
['stripe_account' => 'cus_1KrJdMGUcADgqoEM']
);
上面的代码片段试图从已连接的账户中检索 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。PaymentMethods取代了Sources 和 Cards,成为 Stripe 中表示支付工具的规范方式,但出于向后兼容性的原因,我们仍然需要能够支持这些旧对象。
$pi = $stripe->paymentIntents->create([
'amount' => 1000,
'currency' => 'usd',
// This could be a PaymentMethod, Card or Source ID
'payment_method' => 'card_1LaRQ7GUcADgqoEMV11wEUxU',
]);
如果没有前缀,我们就无法知道 ID 代表什么类型的对象,这意味着我们不知道该查询哪个表来获取对象数据。为了找到一个 ID 而查询每个表效率极低,所以我们需要一个更好的方法。一种方法是要求一个额外的“type”参数:
$pi = $stripe->paymentIntents->create([
'amount' => 1000,
'currency' => 'usd',
// Without prefixes, we'd have to supply a 'type'
'payment_method' => [
'type' => 'card',
'id' => '1LaRQ7GUcADgqoEMV11wEUxU'
],
]);
这虽然可行,但这会使我们的 API 变得复杂,却没有任何额外的好处。payment_method
它不再是一个简单的字符串,而是一个哈希值。此外,这里没有任何不能合并成单个字符串的附加信息。每当使用 ID 时,您都需要知道它代表什么类型的对象,因此将这两种类型的信息合并到一个源中是一个比需要额外的“类型”参数更好的解决方案。
通过前缀,我们可以立即推断出支付工具是 PaymentMethod、Source 还是 Card 之一,并且知道要查询哪个表,尽管它们是完全不同类型的对象。
防止人为错误
前缀还有其他一些不太明显的好处,其中之一就是可以根据前几个字符推断 ID 的类型,从而简化 ID 的使用。例如,在Stripe Discord 服务器上,我们使用 Discord 的AutoMod功能自动标记和屏蔽包含 Stripe Live Secret API 密钥(以 开头)的消息sk_live_
。泄露此类敏感密钥可能会对您的业务造成严重后果,因此我们会采取措施,避免在我们控制的环境中发生这种情况。
通过使用以 开头的键sk_live_
,编写正则表达式来过滤意外泄漏非常简单:
这样,我们可以防止秘密的实时 API 密钥在我们的 Discord 中泄露,但允许以该格式发布测试密钥sk_test_123
(尽管您也应该绝对保密)。
说到 API 密钥,live
和test
前缀是内置的保护层,可以防止混淆。对于特别注重安全的用户,您可以更进一步设置检查,以确保只在合适的环境中使用密钥:
if (preg_match("/sk_live/i", $_ENV["STRIPE_SECRET_API_KEY"])) {
echo "Live key detected! Aborting!";
return;
}
echo "Proceeding in test mode";
Stripe 自 2012 年以来一直在使用这种前缀技术,据我所知,我们是第一批大规模实施该技术的公司。(这是否正确?请在下方评论区告诉我!)。2012 年之前,Stripe 的所有对象 ID 看起来都更像传统的 UUID。如果您是 Stripe 的早期采用者,您可能会注意到您的账户 ID 仍然像这样,只是没有前缀。
补充: IETF比 Stripe 早几年就推出了URN 规范。你在工作中使用 URN 格式吗?告诉我吧!
为人类设计 API
Stripe ID 的结构主要受我们为需要集成 API 的人类开发者设计的理念影响。计算机通常不关心 ID 是什么样子,只要它是唯一的就行。然而,使用这些 ID 进行开发的人类开发者却非常在意,因此我们投入大量精力来提升 API 的开发者体验。
希望本文能让您了解在 ID 中添加前缀的好处。如果您好奇如何有效地实现它(并且恰好使用 Ruby),Chris Oliver 构建了一个gem,可以让您轻松地将其添加到您的系统中。
关于作者
Paul Asjes是 Stripe 的开发倡导者,负责编写代码、编写代码,并主持每月一次的开发者问答系列活动。工作之余,他喜欢酿造啤酒、制作肉干,以及在马里奥赛车游戏中与儿子较量。
文章来源:https://dev.to/stripe/designing-apis-for- humans-object-ids-3o5a