寓言/laravel-cuid2

此包允许您轻松地在Laravel模型中处理Cuid2。

v1.0.2 2024-05-26 04:08 UTC

This package is auto-updated.

Last update: 2024-09-26 05:00:15 UTC


README

注意:此包使用 Cuid2 作为初始版本已弃用。

注意:此包明确不禁止在您的Eloquent模型上禁用自动递增。在数据库索引方面,通常使用自动递增整数进行内部查询更为高效。对 cuid 列进行索引将使该列的查找变得快速,而不会影响相关模型之间的查询。

安全、防碰撞、优化于水平扩展和性能的id。下一代UUID。

在您的应用中需要唯一的id吗?忘记UUID和GUID,它们在大应用中经常发生冲突。使用Cuid2代替。

Cuid2是

  • 安全的:无法猜测下一个id、现有有效id或从id中了解有关引用数据的任何信息。Cuid2使用多个独立的信息源,并使用经过安全审计的NIST标准密码学安全散列算法(Sha3)对其进行散列。
  • 防碰撞的:极不可能生成相同的id两次(默认情况下,您需要生成大约4,000,000,000,000,000,000个id(sqrt(36^(24-1) * 26) = 4.0268498e+18)以达到50%的碰撞概率)。
  • 水平可扩展的:在多台机器上生成id,无需协调。
  • 离线兼容的:无需网络连接即可生成id。
  • URL和名称友好:没有特殊字符。
  • 快速方便:没有异步操作。不会引入用户可感知的延迟。小于5k,gzip压缩。
  • 但不是太快:如果您可以快速散列,则可以发起并行攻击来查找重复项或破坏熵隐藏。对于唯一id,最快的运行者将输掉安全性竞赛。

Cuid2不适合

  • 顺序id(请参阅下面的关于K可排序id的说明
  • 高性能紧密循环,如渲染循环(如果您不需要跨主机的唯一id或安全性,请考虑为此用例使用简单的计数器,或尝试UlidNanoId)。

安装

此包通过Composer安装。要安装,请运行以下命令。

composer require parables/laravel-cuid2

代码示例

为了使用此包,您只需在Eloquent模型中导入并使用特性即可。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Parables\Cuid\GeneratesCuid;

class Post extends Model
{
    use GeneratesCuid;
}

假设您已经在数据库中有一个名为 cuid 的字段,用于存储生成的值。

如果您想使用自定义列名,例如,如果您想将您的主 id 列更改为 Cuid,您可以在您的模型中定义一个静态的 cuidColumn 方法。

class Post extends Model
{
    public static function cuidColumn(): string
    {
        return 'id';
    }
}

使用Cuid作为主键

如果您选择使用Cuid作为您的模型主键(id),则使用 CuidAsPrimaryKey 特性在您的模型上。

<?php

namespace App;

use Parables\Cuid\CuidAsPrimaryKey;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use CuidAsPrimaryKey;

        /**
     * The "type" of the primary key ID.
     *
     * @var string
     */
    protected $keyType = 'string';

    /**
     * Indicates if the IDs are auto-incrementing.
     *
     * @var bool
     */
    public $incrementing = false;

}

并更新您的迁移

 Schema::create('users', function (Blueprint $table) {
-     $table->id();
+     $table->string('id')->primary();
 });

此特性还提供了一个查询范围,您可以使用它轻松地根据其Cuid查找记录,并尊重您选择的任何自定义字段名称。

// Find a specific post using the `cuidColumn()`
$post = Post::whereCuid($cuid)->first();

// Find multiple posts using the `cuidColumn()`
$post = Post::whereCuid([$first, $second])->get();

// Find a specific post with a custom column name
$post = Post::whereCuid($cuid, 'custom_column')->first();

// Find multiple posts with a custom column name
$post = Post::whereCuid([$first, $second], 'custom_column')->get();

路由模型绑定

如果您希望利用隐式路由模型绑定在 cuid 字段上,可以使用 BindsOnCuid 特性,它将使用 cuidColumn() 返回的值。如果您需要更多的绑定控制,可以直接重写 getRouteKeyName 方法。

public function getRouteKeyName(): string
{
    return 'cuid';
}

您可以通过在 cuidColumns() 方法中返回列名数组来为模型生成多个 Cuid 列。

如果您使用 cuidColumns() 方法,则数组中的 第一个 元素必须是 cuidColumn() 方法返回的值,默认为 cuid。如果您重写了 cuidColumn() 方法,请将其返回值作为 cuidColumns() 返回数组的 第一个 元素。

当使用 whereCuid() 范围查询时,将使用由 cuidColumn() 指定的默认列。

class Post extends Model
{
    public static function cuidColumns(): array
    {
        return [$this->cuidColumn(), 'custom_column'];
    }
}

cuidColumns 必须返回一个数组。您可以自定义每个列生成的 Cuid。将列名指定为键,并可选地指定一个大于 4 但小于 32 的整数 maxLength((($maxLength > 4) && ($maxLength < 32)))。

  public static function cuidColumns(): array
    {
        // Option 1: array of column names: this will use the default size: `24`
        return ['cuid', 'custom_column'];

        // Option 2: array where the key is the column name and the value is the size
        return ['cuid' => 6, 'custom_column' => 10];

    }

要全局设置默认 maxLength,请在 config/app.php 中添加此内容

<?php

// config/app.php

+    /*
+    |--------------------------------------------------------------------------
+    | Cuid2 Size
+    |--------------------------------------------------------------------------
+    |
+    | This value determines the size of Cuid2. Set this in your ".env" file.
+    |
+    */
+
+    'cuid_size' => env('CUID_SIZE', 10),

最后,将其添加到 .env 文件中

# .env

+ CUID_SIZE=10

为什么选择 Cuid2?

默认情况下,ID 应该是安全的,原因与浏览器会话默认应该是安全的一样。如果它们不安全,会出现很多问题,不安全的 ID 可能以意想不到的方式导致问题,包括未经授权的用户 账户访问重置密码功能未经授权访问用户数据,以及可能导致灾难性后果的个人信息意外泄露(例如,在看似无辜的应用程序,如健身跑步追踪器中,参见 2018 年 Strava 五角大楼泄露PleaseRobMe)。

并非所有安全措施都应被视为同等重要。例如,信任浏览器 "Cryptographically Secure" Psuedo Random Number Generator (CSPRNG)(如 uuidnanoid 工具中使用的)不是一个好主意。例如,浏览器 CSPRNG 可能存在 漏洞。多年来,Chromium 的 Math.random() 并不真正随机。Cuid 的创建是为了解决 ID 生成器中不可信熵的问题,这导致了生产环境中频繁的 ID 冲突和相关问题。Cuid2 通过结合多个熵源来提供比其他解决方案更强的安全性和冲突抵抗保证。

现代网络应用的需求与在 GUID(全局唯一标识符)和 UUID(通用唯一标识符)早期编写的应用程序不同。特别是,Cuid2 旨在提供比任何现有 GUID 或 UUID 实现更强的唯一性保证,并防止泄露有关所引用数据或生成 ID 的系统的任何信息。

Cuid2 是 Cuid 的下一代,它已在一万个应用程序中使用了十多年,没有已知的冲突报告。Cuid2 中的更改是显著的,可能会破坏许多依赖 Cuid 的项目,因此我们决定创建一个替代库和 ID 标准。现在,Cuid 已弃用,Cuid2 取而代之。

翻滚,意味着在循环中生成足够多的ID时,它们可能会相互碰撞,从而降低其有效性。Cuid2使用随机数初始化计数器,因此熵永远不会浪费。它还使用了原生JS数字类型的完整精度。如果您只生成一个ID,计数器只是扩展随机熵,而不是浪费数字,从而提供更强的防碰撞保护。

参数化长度

不同的用例对熵抵抗的需求不同。有时,一串短的随机数字就可以足够:例如,通常使用短别名来区分相似的名字,例如用户名或URL别名。由于原始的cuid没有对输出进行哈希处理,我们不得不做出一些严重的限制性熵决策来生成短别名。在新版本中,所有熵来源都被混合到哈希函数中,您可以安全地抓取任意长度小于32位的子串。您可以通过以下公式大致估算在达到50%碰撞几率之前可以生成多少ID:sqrt(36^(n-1)*26),因此如果使用4位数字,那么在仅生成~1101个ID之后就会达到50%的碰撞几率。这或许对于用户名去重来说是足够的。有多少人想要使用相同的用户名?

默认情况下,您需要生成~4.0268498e+18个ID才能达到50%的碰撞几率,在最大长度下,您需要生成~6.7635614e+24个ID才能达到50%的碰撞几率。要使用自定义长度,导入init函数,它接受配置选项

import { init } from "@paralleldrive/cuid2";
const length = 10; // 50% odds of collision after ~51,386,368 ids
const cuid = init({ length });
console.log(cuid()); // nw8zzfaa4v

增强安全性

原始的Cuid会泄露有关ID的详细信息,包括来自主机环境(通过主机指纹)的非常有限的数据和ID创建的确切时间。新的Cuid2将所有熵来源哈希成看起来随机的字符串。

由于哈希算法,从生成的ID中恢复任何熵来源实际上是不可能的。Cuid由于数据库性能原因使用大致单调递增的ID。有些人滥用它们通过创建日期选择数据。如果您想要能够按创建日期对项目进行排序,我们建议在数据库中创建一个独立的、索引的createdAt字段,而不是使用单调ID,因为

  • 很容易欺骗客户端系统生成过去或未来的ID。
  • 在多个主机几乎同时生成ID的情况下,顺序不保证。
  • 确定性单调解析从未得到保证。

在Cuid2中,哈希算法使用了盐。盐是一个随机字符串,在应用哈希函数之前添加到输入熵源中。这使得攻击者猜测有效ID变得更加困难,因为盐与每个ID一起变化,这意味着攻击者无法使用任何现有ID作为猜测其他ID的基础。使用序列键在现代系统中的影响通常被夸大。**如果您的数据库太小而无法使用云原生解决方案,它也太小而无需担心序列ID与随机ID的性能影响,除非您生活在遥远的过去(即您使用的是2010年的硬件)。如果它足够大而需要担心,随机ID仍然可能更快。

在过去,序列键可能会对性能产生重大影响,但在现代系统中,情况并非如此。

使用序列键的原因之一是避免ID碎片化,这可能会需要大量磁盘空间来存储拥有数十亿记录的数据库。然而,在如此大的规模上,现代系统通常使用设计用于以高效和低成本处理数以TB计数据的云原生数据库。此外,整个数据库可能存储在内存中,提供快速随机访问查找性能。因此,碎片化键对性能的影响微乎其微。

更糟糕的是,K-Sortable ids 并非总对性能有益,因为它们可能导致数据库中出现热点。如果您有一个在短时间内生成大量id的系统,这些id将以顺序生成,导致树变得不平衡,进而需要频繁地进行平衡,这可能会对性能产生重大影响。

那么哪些操作会受到非顺序id的影响呢?分页、排序操作。例如,“按id顺序获取100000条记录”。这将会受到显著影响,但如果你使用的id是不可见的,你通常不需要按id排序。现代云数据库允许您在createdAt字段上创建索引,这些索引的性能非常出色。

K-Sortable ids 的最坏部分在于其对安全性的影响。K-Sortable = 不安全。

竞争者

我们不清楚在这个领域有任何标准或库能够充分满足我们的所有要求。我们将使用以下标准来筛选常用替代方案列表。让我们从竞争者开始。

数据库自增(Int, BigInt, AutoIncrement),UUID v1 - v8NanoIdUlidSony Snowflake(受Twitter Snowflake启发),ShardingID(Instagram),KSUIDPushId(Google),XIDObjectId(MongoDB)。

以下是我们关心的不合格因素

  • 泄露信息:数据库自增,所有UUID(除V4外,包括V6 - V8),Ulid,Snowflake,ShardingId,pushId,ObjectId,KSUID
  • 碰撞概率高:数据库自增,v4 UUID
  • 非密码学安全的随机输出:数据库自增,UUID v1,UUID v4
  • 需要分布式协调:Snowflake,ShardingID,数据库自增
  • 不适合URL或名称:UUID(太长,有破折号),Ulid(太长),UUID v7(太长)——任何支持特殊字符(如破折号、空格、下划线、#$%^&等)的方案。
  • 速度太快:UUID v1,UUID v4,NanoId,Ulid,Xid

以下是我们关心的合格因素

  • 安全——无泄露信息,具有抗攻击性:Cuid2,NanoId(中等——信任Web密码学API熵)。
  • 抗碰撞性:Cuid2,Cuid v1,NanoId,Snowflake,KSUID,XID,Ulid,ShardingId,ObjectId,UUID v6 - v8。
  • 水平可扩展性:Cuid2,Cuid v1,NanoId,ObjectId,Ulid,KSUID,Xid,ShardingId,ObjectId,UUID v6 - v8。
  • 离线兼容性:Cuid2,Cuid v1,NanoId,Ulid,UUID v6 - v8。
  • URL和名称友好:Cuid2,Cuid v1,NanoId(使用自定义字母表)。
  • 快速便捷:Cuid2,Cuid v1,NanoId,Ulid,KSUID,Xid,UUID v4,UUID v7。
  • 但不是太快Cuid2,Cuid v1,UUID v7,Snowflake,ShardingId,ObjectId。

Cuid2 是唯一通过我们所有测试的解决方案。

NanoId 和 Ulid

总的来说,NanoId和Ulid似乎满足了我们的大部分需求,但Ulid会泄露时间戳,而且它们都对Web Crypto API提供的随机熵过于信任。Web Crypto API信任了两件不可信的事情:一个是随机熵源,另一个是用于将熵拉伸成看似随机的数据的哈希算法。一些实现存在严重的漏洞,使其容易受到攻击

除了使用密码学安全方法,Cuid2还提供来自多样化池的已知熵,并使用经过安全审计的NIST标准密码学安全哈希算法。

太快了:NanoId和Ulid也非常快。但这并不是好事。你生成ID的速度越快,你进行碰撞攻击的速度就越快。寻找ID分布中统计异常的坏人可以利用NanoId的速度。Cuid2足够快,方便使用,但不会带来安全风险。

熵安全性比较

  • NanoId熵:Web Crypto。
  • Ulid熵:Web Crypto + 时间戳(泄露)。
  • Cuid2熵:Web Crypto + 时间戳 + 计数器 + 主机指纹 + 哈希算法。

比较

安全性是创建Cuid2的主要动机。我们的ID应该像我们使用https而不是http一样默认安全。问题是,我们目前的ID规范都是基于几十年前的标准,这些标准从未考虑过安全性,优化的是数据库性能特性,这在现代分布式应用中已不再相关。今天几乎所有流行的ID都优化了k-sortable,这在十年前很重要。这里解释一下k-sortable的含义,以及为什么它不再像我们创建Cuid规范时那么重要,当时它启发了当前标准如UUID v6 - v8

关于K-Sortable/顺序/单调递增ID的说明

TL;DR:别再担心K-Sortable ID了。这已经不再重要。用createdAt字段代替。

熵性能是一个衡量系统中总信息量的指标。在唯一ID的上下文中,更高的熵会导致碰撞更少,也可以使攻击者更难猜测有效的ID。

Cuid2由以下熵源组成

  • 一个初始字母,使ID在JavaScript和HTML/CSS中成为可用的标识符
  • 当前系统时间
  • 伪随机值
  • 会话计数器
  • 主机指纹

字符串是Base36编码的,这意味着它只包含小写字母和数字:0 - 9,没有特殊符号。

水平可扩展性

今天的应用程序不运行在任何单一机器上。

应用程序可能需要支持在线/离线功能,这意味着我们需要一种方式,让不同主机上的客户端生成ID,这些ID不会与其他主机生成的ID冲突——即使它们没有连接到网络。

大多数伪随机算法使用毫秒为单位的时间作为随机种子。当在单独的进程(如克隆的虚拟机或客户端浏览器)中运行时,随机ID缺乏足够的熵以保证不会发生冲突。应用程序开发人员报告,当ID生成在大量机器之间分布,大量ID在同一毫秒内生成时,v4 UUID的碰撞会导致他们的应用程序出现问题。

每个新的客户端都会以指数级增加在同一时间内发生碰撞的可能性,就像随机字符串中的每个新字符都会以指数级减少碰撞的可能性一样。成功的应用程序每天可以扩展数百或数千个新客户端,所以通过添加随机字符来克服熵不足是制造极长标识符的方案。

由于这个问题本身的性质,在发现问题之前,你可能已经从头开始构建了一个应用,并且可以扩展到一百万用户。当你注意到这个问题(在高峰时段使用时,每毫秒需要创建数十个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种不同的语言实现)。

端口

相对于Cuid的改进

原始的Cuid为我们服务了十多年。我们将其应用于两个不同的社交网络,以及为Adobe Creative Cloud生成id。在使用过程中,我们没有遇到过任何碰撞问题。但仍有改进的空间。

更好的碰撞抵抗性

可用的熵是能生成的最大唯一id数量。通常,更多的熵会导致碰撞概率更低。为了简化讨论,我们将假设以下讨论中的完美随机分布。

原始Cuid在数千个软件实现中运行了超过10年,没有确认过任何碰撞报告,在某些情况下,有超过1亿用户生成id。

原始Cuid的最大可用熵约为3.71319E+29(假设每会话一个id)。这已经是一个非常大的数字,但Cuid2中建议的最大熵为4.57458E+49。为了参考,这大约与一只蚊子与地球到最近恒星之间的距离相当。Cuid2的默认熵为1.62155E+37,比原始Cuid有显著提高,相当于棒球与月亮大小的差距。

散列函数将所有熵源混合成一个单一值,因此使用高质量的散列算法非常重要。我们使用Cuid2测试了数十亿个id,迄今为止未检测到任何碰撞。

更便携

原始的Cuid在浏览器、Node和React Native等不同类型的宿主上使用不同的方法生成指纹。不幸的是,这导致了Cuid用户生态系统中存在一些兼容性问题。

在Node中,每个生产宿主都有细微的差异,我们可以可靠地获取进程id等来区分宿主。当我们开始在云虚拟主机上部署,使用相同的容器和微容器架构时,我们关于不同宿主在Node中生成不同PID的早期假设被证明是错误的。结果是,Node中的宿主指纹熵较低,限制了它们在云工作者和微容器等环境中提供良好的碰撞抵抗性,以支持横向服务器扩展。

如果使用Cuid,您也无法自定义指纹函数,以满足不同的指纹需求,例如,如果globalwindow都是undefined

Cuid2使用了JavaScript环境中的所有全局名称列表。对其进行哈希处理可以产生非常好的宿主指纹,但我们故意没有在原始的Cuid中包含哈希函数,因为我们能找到的所有安全哈希函数都会使包膨胀,因此原始的Cuid无法充分利用所有独特的宿主熵。

在Cuid2中,我们使用了一个微小、快速、经过安全审计的NIST标准化的哈希函数,并用随机熵对其进行初始化,因此在全局变量都相同的生产环境中,我们失去了独特的指纹,但仍然得到了随机熵来替代它,增强了抗碰撞能力。

确定长度

Cuid的长度是不确定的。这在几乎所有情况下都工作得很好,但对于某些数据结构的使用来说却成了问题,迫使一些用户编写包装代码来填充输出。我们建议在大多数情况下坚持默认设置,但如果您不需要强大的唯一性保证(例如,您的用例类似于用户名或URL去歧义),使用较短版本是可以的。

更高效的会话计数器熵

原始的Cuid在很少使用、很少填充且有时未使用的会话计数器上浪费了熵。

支持

如果您在使用此包时遇到一般问题,请随时在Twitter上联系我。

如果您认为您找到了一个问题,请使用GitHub问题跟踪器报告它,或者更好的是,分叉存储库并提交一个pull request。

如果您正在使用此包,我很想听听您的想法。谢谢!

TreeWare

您可以使用此包,但如果它进入您的生产环境,您必须为世界买一棵树。

众所周知,应对气候危机并防止气温上升超过1.5C的最佳工具之一是种树。如果您支持此包并贡献给TreeWare森林,您将为当地家庭创造就业机会并恢复野生动物栖息地。

您可以在这里购买树木。

更多关于TreeWare的信息请访问treeware.earth

鸣谢