安全、抗碰撞的唯一标识符,针对水平扩展和性能进行了优化。下一代UUID。
您的应用需要唯一标识符吗?忘掉在大型应用中经常发生碰撞的UUID和GUID吧。使用Cuid2来代替。
Cuid2具有以下特点:
Cuid2不适合:
npm install --save @paralleldrive/cuid2
或
yarn add @paralleldrive/cuid2
import { createId } from '@paralleldrive/cuid2'; const ids = [ createId(), // 'tz4a98xxat96iws9zmbrgj3a' createId(), // 'pfh0haxfpzowht3oi213cqos' createId(), // 'nc6bzmkmd014706rfda898to' ];
使用Jest?跳转至在Jest中使用。
import { init } from '@paralleldrive/cuid2'; // init函数返回一个具有指定配置的自定义createId函数。 // 所有配置属性都是可选的。 const createId = init({ // 具有与Math.random相同API的自定义随机函数。 // 您可以使用它来传递加密安全的随机函数。 random: Math.random, // id的长度 length: 10, // 主机环境的自定义指纹。用于帮助防止在分布式系统中生成id时发生碰撞。 fingerprint: 'a-custom-host-fingerprint', }); console.log( createId(), // wjfazn7qnd createId(), // cerhuy9499 createId(), // itp2u4ozr4 );
import { createId, isCuid } from '@paralleldrive/cuid2'; console.log( isCuid(createId()), // true isCuid('not a cuid'), // false );
默认情况下,id应该是安全的,原因与浏览器会话默认应该安全相同。不安全的id可能会导致许多问题,并可能以意想不到的方式造成问题,包括未经授权的用户账户访问、未经授权访问用户数据,以及意外泄露用户个人数据,这可能导致灾难性后果,即使在看似无害的应用程序中也是如此,如健身跑步追踪器(参见2018年Strava五角大楼数据泄露事件和PleaseRobMe)。
并非所有安全措施都应被视为等同。例如,不应该信任浏览器的"加密安全"伪随机数生成器(CSPRNG)(在uuid和nanoid等工具中使用)。例如,浏览器CSPRNG可能存在漏洞。多年来,Chromium的Math.random()
根本就不随机。Cuid的创建是为了解决id生成器中不可信熵的问题,这导致了生产应用中频繁的id碰撞和相关问题。Cuid2不依赖单一熵源,而是结合多个熵源,以提供比其他解决方案更强的安全性和抗碰撞保证。
现代Web应用程序的需求与GUID(全局唯一标识符)和UUID(通用唯一标识符)早期编写的应用程序不同。特别是,Cuid2旨在提供比任何现有GUID或UUID实现更强的唯一性保证,并防止泄露有关被引用数据或生成id的系统的任何信息。
Cuid2是Cuid的下一代,Cuid已在数千个应用程序中使用了十多年,没有确认的碰撞报告。Cuid2的变化很大,可能会影响许多依赖Cuid的项目,因此我们决定创建一个替代库和id标 准。Cuid现已被弃用,推荐使用Cuid2。
熵是系统中总信息量的度量。在唯一id的上下文中,更高的熵会导致更少的碰撞,并且也可能使攻击者更难猜测有效的id。
Cuid2由以下熵源组成:
字符串采用Base36编码,这意味着它只包含小写字母和数字:0 - 9,没有特殊符号。
今天的应用程序不再运行在单一机器上。
应用程序可能需要支持在线/离线功能,这意味着我们需要一种方法,使不同主机上的客户端能够生成不会与其他主机生成的id发生碰撞的id - 即使它们没有连接到网络。
大多数伪随机算法使用毫秒级时间作为随机种子。当在单独的进程(如克隆的虚拟机或客户端浏览器)中运行时,随机ID缺乏足够的熵来保证不会发生碰撞。应用程序开发人员报告,当ID生成分布在大量机器上,以至于在同一毫秒内生成大量ID时,v4 UUID碰撞会导致应用程序出现问题。 每个新客户端都会以指数级增加冲突的可能性,就像随机字符串中每增加一个字符都会以指数级减少冲突的可能性一样。成功的应用每天会新增数百或数千个客户端,因此通过添加随机字符来对抗熵的缺乏将导致标识符变得ridiculously长。
由于这个问题的性质,可能在构建应用并将其扩展到百万用户之前都不会发现这个问题。当你注意到问题时(当高峰时段每毫秒需要创建数十个ID时),如果你的数据库没有对ID设置唯一约束(因为你认为你的GUID是安全的),你就会陷入困境。你的用户开始看到不属于他们的数据,因为数据库只返回它找到的第一个ID匹配项。
另一种情况是,你采取了 安全措施,只让数据库创建ID。写操作只在主数据库上进行,负载分散在只读副本上。但在这种压力下,你必须开始水平扩展数据库写操作,突然你的应用开始变慢(如果数据库足够智能,能保证写入主机之间的ID唯一性),或者你开始在不同的数据库主机之间遇到ID冲突,导致你的写入主机对哪些ID代表哪些数据产生分歧。
ID生成应该足够快,以至于人类不会注意到延迟,但又要足够慢,使得暴力破解(即使并行)变得不可行。这意味着不能等待异步熵池请求或跨进程/跨网络通信。在浏览器中性能会慢到不切实际。所有熵源都需要快到可以同步访问。
更糟糕的是,当数据库是保证ID唯一性的唯一保证时,这意味着客户端被迫向数据库发送不完整的记录,并等待网络往返才能在任何算法中使用这些ID。忘掉快速的客户端性能吧。这根本不可能。
这种情况导致一些客户端创建只能在单个客户端会话中使用的ID(如内存计数器)。当数据库返回真实ID时,客户端必须进行一些杂耍逻辑来替换正在使用的ID,增加了客户端实现代码的复杂性。
如果客户端ID生成更强大,冲突的机会会小得多,客户端可以向数据库发送完整的记录以插入,而无需等待完整的往返请求就能使用ID。
页面加载需要快速,这意味着我们不能在复杂的算法上浪费大量JavaScript。Cuid2非常小。这对于重客户端JavaScript应用尤其重要。
客户端可见的ID通常需要有足够的随机数据和熵,使得根据已知的ID猜测有效ID变得几乎不可能。这使得简单的顺序ID在客户端生成数据库键的情况下无法使用。此外,使用V4 UUID也不安全,因为对于几种ID生成算法,有已知的攻击方法,复杂的攻击者可以用来预测下一个ID。Cuid2已经经过安全专家和人工智能的审核,被认为可以安全地用于秘密分享链接等用例。
大多数更强大的UUID / GUID算法需要访问浏览器中不可用的操作系统服务,这意味着它们无法按规范实现。此外,我们的ID标准需要可移植到多种语言(原始cuid有22种不同的语言实现)。
[列出了各种语言的Cuid2实现,包括Clojure、ColdFusion、Dart、Java、.NET、PHP、Python、Ruby和Rust]
原始的Cuid在超过十年的时间里为我们提供了良好的服务。我们在两个不同的社交网络中使用它,并用它为Adobe Creative Cloud生成ID。在使用它的生产系统中,我们从未遇到过冲突问题。但仍有改进的空间。
可用熵是可以生成的唯一ID的最大数量。通常更多的熵会导致更低的冲突概率。为简单起见,我们在以下讨论中假设一个完美的随机分布。
原始的Cuid在数千个软件实现中运行了超过10年,没有确认的冲突报告,在某些情况下有超过1亿用户生成ID。
原始Cuid的最大可用熵约为3.71319E+29(假设每个会话1个ID)。这已经是一个非常大的数字,但Cuid2的最大推荐熵为4.57458E+49。作为参考,这种熵的差异大约相当于蚊子的大小与地球到最近恒星的距离之间的差异。Cuid2的默认熵为1.62155E+37,这比原始Cuid有显著增加,可以比作棒球大小和月球大小之间的差异。
哈希函数将所有熵源混合成一个单一值,因此使用高质量的哈希算法很重要。我们已经用Cuid2测试了数十亿个ID,迄今为止没有检测到冲突。
原始Cuid在不同类型的主机(包括浏览器、Node和React Native)上使用不同的方法来生成指纹。不幸的是,这在cuid用户生态系统中造成了几个兼容性问题。
在Node中, 每个生产主机略有不同,我们可以可靠地获取进程ID等来区分主机。但当我们开始在使用相同容器和微容器架构的云虚拟主机上部署时,我们早期关于不同主机在Node中生成不同PID的假设被证明是错误的。结果是Node中的主机指纹熵很低,限制了它们在云工作者和微容器等环境中为水平服务器扩展提供良好冲突抵抗的能力。
如果你有不同的指纹需求,例如当global和window都是undefined时,也无法使用Cuid自定义你的指纹函数。
Cuid2使用JavaScript环境中所有全局名称的列表。对其进行哈希处理可以产生非常好的主机指纹,但我们故意没有在原始Cuid中包含哈希函数,因为我们能找到的所有安全哈希函数都会增加包的大小,所以原始Cuid无法充分利用所有这些独特的主机熵。
在Cuid2中,我们使用一个微小、快速、经过安全审核的NIST标准化哈希函数,并用随机熵对其进行种子处理,因此在所有全局变量都相同的生产环境中,我们失去了唯一的指纹,但仍然获得随机熵来替代它,增强了冲突抵抗能力。
Cuid的长度是不确定的。这在大多数情况下运作良好,但对于某些数据结构的使用却成为了问题,迫使一些用户创建包装代码来填充输出。我们建议在大多数情况下坚持使用默认设置,但如果你不需要强大的唯一性保证(例如,你的用例是类似用户名或URL消歧),使用较短的版本也可以。
原始的Cuid在会话计数器上浪费了熵,这些计数器并不总是被使用,很少被填满,有时还会翻转,意味着如果你在短时间内生成足够多的ID,它们可能会相互冲突,从而降低其有效性。Cuid2用随机数初始化计数器,所以熵永远不会被浪费。它还使用了原生JS数字类型的全精度。如果你只生成一个ID,计数器就只是扩展了随机熵,而不是浪费数字,提供了更强的防冲突保护。
不同的用例对熵抗性有不同的需求。有时,一串短的随机数字就足够用于消歧:例如,常见的做法是使用短标签来区分相似的名称,如用户名或URL标签。由于原始cuid没有对其输出进行哈希处理,我们不得不做出一些严重限制熵的决定来生成短标签。在新版本中,所有熵源都与哈希函数混合,你可以安全地获取任何短于32位的子字符串。你可以粗略估计在达到50%碰撞概率之前可以生成多少个ID:[sqrt(36^(n-1)*26)
],所以如果你使用4位数,在生成约1101个ID后就会达到50%的碰撞概率。这对用户名消歧可能已经足够了。真的会有超过1000人想要使用相同的用户名吗?
默认情况下,你需要生成约4.0268498e+18个ID才能达到50%的碰撞概率,而在最大长度下,你需要生成约6.7635614e+24个ID才能达到50%的碰撞概率。要使用自定义长度,请导入init
函数,它接受配置选项:
import { init } from '@paralleldrive/cuid2'; const length = 10; // 生成约51,386,368个ID后达到50%的碰撞概率 const cuid = init({ length }); console.log(cuid()); // nw8zzfaa4v
原始的Cuid泄露了ID的详细信息,包括来自主机环境的非常有限的数据(通过主机指纹),以及创建ID的确切时间。新的Cuid2将所有熵源哈希成一个看似随机的字符串。
由于哈希算法的存在,从生成的ID中恢复任何熵源应该是不可能的。Cuid出于数据库性能原因使用了大致单调递增的ID。有些人滥用它们来按创建日期选择数据。如果你想能够按创建日期排序项目,我们建议在数据库中创建一个单独的、已索引的createdAt
字段,而不是使用单调ID,原因如下:
在Cuid2中,哈希算法使用了盐值。盐值是一个随机字符串,在应用哈希函数之前添加到输入熵源中。这使得攻击者更难猜测有效的ID,因为每个ID的盐值都会改变,意味着攻击者无法使用任何现有的ID作为猜测其他ID的基础。
创建Cuid2的主要动机是安全性。我们的ID应该默认安全,就像我们使用https而不是http一样。问题是,我们所有当前的ID规范都基于几十年前的标准,这些标准从未考虑到安全性,而是优化了在现代分布式应用中不再相关的数据库性能特征。今天几乎所有流行的ID都在优化k-sortable特性,这在10年前很重要。以下是k-sortable的含义,以及为什么它不再像我们创建Cuid规范时那样重要,该规范[帮助启发了当前的标准,如UUID v6 - v8]: