【问题标题】:Random token generation - a supposedly unlikely collision occurred随机令牌生成 - 发生了本应不太可能发生的冲突
【发布时间】:2018-06-09 10:01:15
【问题描述】:

几个月前,我们使用 UUID 来生成需要全面唯一的随机字符串 ID。然后我更改了算法,以便在我们的数据库中保存一些数据和索引空间。我测试了几种生成唯一字符串 ID 的方法,我决定使用这个函数:

function generateToken($length) {
    $characters = '0123456789abcdefghijklmnopqrstuvwxyz';
    $max = strlen($characters) - 1;

    $token = '';
    for ($i = 0; $i < $length; $i++) {
        $token .= $characters[mt_rand(0, $max)];
    }

    return $token;
}

我正在使用此函数生成使用数字和字母的 20 个字符长的 ID,或者您可以说这些 ID 是以 36 为底的数字。任何 2 个 ID 冲突的概率应该是 1/36^20,但是由于生日悖论,可以预计在大约 36^10 条记录之后发生碰撞 - 即 3.6 万亿条记录。然而,就在几个小时前发生了冲突,当时数据库中只有 530 万条现有记录。我是不是很倒霉,还是我的 ID 生成功能在随机性方面存在缺陷?我知道 mt_rand() 并不是真正随机的,但它足够随机,不是吗?

我会编写一个循环来检查生成的 ID 是否唯一,如果不是,则生成一个新 ID,但我认为发生冲突的机会非常小,以至于这样的循环的性能成本不值得。我现在将在代码中包含这样一个循环,但如果确实存在缺陷,我仍然有兴趣完善 ID 生成功能。

【问题讨论】:

  • 大多数数据库管理系统都具有生成 uuid 的功能,这些 uuid 保证对于特定数据库实例是唯一的。为什么不使用这些?
  • 由于空间限制,我将 UUID 换成了这种基于 36 个 ID 的 ID。我需要将尽可能多的信息打包到一点数据库空间中,同时仍然使用一种生成足够长且足够复杂的 ID 的算法,以使冲突极不可能发生。 UUID 以 16 为基数,包含破折号和一些非随机字符,因此它们的空间效率不如我所愿。
  • 你描述的只是一个uuid的字符串表示。 uuid 实际上是一个大小正好为 16 字节的二进制结构。大多数 DBMS 专门支持 uuid 列类型,然后存储 16 字节二进制表示,而不是字符串表示。您使用什么 DBMS?
  • 我只是读了一点 mt_rand 基数,我很难相信这是基于随机数生成器的。你是如何产生种子的?可能会发生碰撞吗?例如,将时间作为种子并并行执行程序多次?
  • MySQL 不直接支持 uuid 列类型,但您可以将 uuid 存储在 varbyte(16) 列中,并使用内置函数 uuid_to_bin / bin_to_uuid 在字符串和二进制表示之间进行转换。跨度>

标签: random token uuid birthday-paradox


【解决方案1】:

mt_rand() 在 PHP 中的实现相当流畅,因此它可能会因版本而异。但是,这里有一些 PHP 版本 5 中使用的代码的摘录:

php_rand.h:

/* MT Rand */
#define PHP_MT_RAND_MAX ((long) (0x7FFFFFFF)) /* (1<<31) - 1 */ 

#ifdef PHP_WIN32
#define GENERATE_SEED() (((long) (sapi_get_request_time(TSRMLS_C) * GetCurrentProcessId())) ^ ((long) (1000000.0 * php_combined_lcg(TSRMLS_C))))
#else
#define GENERATE_SEED() (((long) (sapi_get_request_time(TSRMLS_C) * getpid())) ^ ((long) (1000000.0 * php_combined_lcg(TSRMLS_C))))
#endif

PHPAPI void php_srand(long seed TSRMLS_DC);
PHPAPI long php_rand(TSRMLS_D);
PHPAPI void php_mt_srand(php_uint32 seed TSRMLS_DC);
PHPAPI php_uint32 php_mt_rand(TSRMLS_D);

rand.c:

PHP_FUNCTION(mt_rand)
{
    long min;
    long max;
    long number;
    int  argc = ZEND_NUM_ARGS();

    if (argc != 0) {
        if (zend_parse_parameters(argc TSRMLS_CC, "ll", &min, &max) == FAILURE) {
            return;
        } else if (max < min) {
            php_error_docref(NULL TSRMLS_CC, E_WARNING, "max(%ld) is smaller than min(%ld)", max, min);
            RETURN_FALSE;
        }
    }

    if (!BG(mt_rand_is_seeded)) {
        php_mt_srand(GENERATE_SEED() TSRMLS_CC);
    }

从上面的最后三行可以看出,mt_rand() 在第一次调用时会自动播种。但是,php_mt_srand() 函数采用php_uint32 类型的参数。 这意味着mt_rand() 只有 232 个可能的种子状态。因此,如果您的脚本运行大约 216 次,则很有可能mt_rand() 将产生完全相同的随机数序列。

正如 rossum 所建议的,将 AES 加密应用于递增的 128 位值会是一个更好的主意。如果您对加密结果进行 base64 编码并丢弃尾随的 ==,则生成的字符串将只有 22 个字符长。


附录

我今天下午外出时让以下脚本运行:

for i in $(seq 1 100000) ; do
  php -r 'for ($n=0; $n<32; $n++) echo chr(mt_rand(97,122)); echo chr(10);' >>out
done &

正如预期的那样,第一次碰撞发生在大约 216 次迭代之后(远不及 2616):

$ sort <out | uniq -d
vnexqclzkaluntglgadgwzjnjfsvqfhp

$ grep -n vnexqclzkaluntglgadgwzjnjfsvqfhp out
34417:vnexqclzkaluntglgadgwzjnjfsvqfhp
52159:vnexqclzkaluntglgadgwzjnjfsvqfhp

【讨论】:

  • 这很有趣。 random_int() 呢?就随机性而言,它是否比 mt_rand() 更好?
  • @Jeff 好多了。根据documentationrandom_int() 默认从/dev/urandom 获取随机字节,因此 20 位 base-36 数字之间发生冲突的可能性非常小。但当然不是零。如果碰撞会产生严重后果,请改用 AES 加密。
  • 仅供参考,在单调递增值上使用 ECB 模式称为计数器 (CTR) 模式,您的加密库可能对此有直接支持。请注意,建议将前 64 位设置为随机值(nonce)。
【解决方案2】:

如果您想要保证唯一的 16 字节 ID,那么我会使用加密。 AES 使用 16 字节(128 位)块,只要输入是唯一的,输出也保证是唯一的。

在 ECB 模式下设置 AES(更简单、更快)并加密数字 0、1、2、3、4,...您的输入是唯一的,因此输出也将是唯一的。

加密网站会告诉您 ECB 模式存在安全问题,但这些问题仅适用于输入不唯一的情况。对于您需要的唯一“随机”数字生成,这些问题不适用,因为您的输入都是唯一的。

【讨论】:

    猜你喜欢
    • 2013-11-26
    • 1970-01-01
    • 2010-09-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-17
    • 1970-01-01
    相关资源
    最近更新 更多