随机令牌生成 - 假设不太可能发生碰撞

时间:2018-06-09 10:01:15

标签: random token uuid birthday-paradox

几个月前,我们使用UUID生成随机字符串ID,这些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生成功能。

2 个答案:

答案 0 :(得分:3)

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

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

加密网站会告诉您ECB模式存在安全问题,但这些问题仅在输入不唯一时才适用。对于独特的&#39;随机&#39;数字生成,根据您的要求,这些问题不适用,因为您的输入都是唯一的。

答案 1 :(得分:3)

PHP中mt_rand()的实现相当流畅,因此它可能因版本而异。但是,以下是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()只有2个 32 可能的种子状态。因此,如果你的脚本大约运行2次 16 次,那么mt_rand()很可能产生完全相同的随机数序列。

正如rossum所建议的那样,将AES加密应用于递增的128位值会更好。如果您对加密结果进行64位编码并丢弃尾随==,则生成的字符串将只有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 &

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

$ sort <out | uniq -d
vnexqclzkaluntglgadgwzjnjfsvqfhp

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