PHP 生成无限唯一标识符的函数

PHP Function to generate an infinite unique indentifier

我正在 Zend 3 中创建一个新项目,它要求我有一个唯一的 ID 或 HASH,我以后可以在几个地方使用它。

我在Google上看了很多例子,找不到一个能满足我要求的函数,因为这需要始终保持99%的唯一性,而且它需要能够生成数百个,数以百万计的 "hashes" 一直都是独一无二的。

以下函数引起了我的注意:

  function uniqidReal($lenght = 13) {
        // uniqid gives 13 chars, but you could adjust it to your needs.
        if (function_exists("random_bytes")) {
            $bytes = random_bytes(ceil($lenght / 2));
        } elseif (function_exists("openssl_random_pseudo_bytes")) {
            $bytes = openssl_random_pseudo_bytes(ceil($lenght / 2));
        } else {
            throw new Exception("no cryptographically secure random function available");
        }
        return substr(bin2hex($bytes), 0, $lenght);
    }

一个简单的测试:

echo "<pre>";

for($i = 0; $i < 100; $i++)
{
    echo $this->uniqidReal(25) .PHP_EOL ;   
}

结果:

a8ba1942ad99d09f496d3d564
5b24746d09cada4b2dc9816bd
c6630c35bc9b4ed0907c803e0
48e04958b633e8a5ead137bb1
643a4ce1bcbca66cea397e85e
d2cd4c6f8dc7054dd0636075f
d9c78bae38720b7e0cc6361f2
54e5f852862adad2ad7bc3349
16c4e42e4f63f62bf9653f96e
c63d64af261e601e4b124e38f
29a3efa07a4d77406349e3020
107d78fdfca13571c152441f2
591b25ebdb695c8259ccc7fe9
105c4f2cc5266bb82222480ba
84e9ad8fd76226f86c89c1ac1
39381d31f494d320abc538a8e
7f8141db50a41b15a85599548
7b15055f6d9fb1228b7438d2a
659182c7bcd5b050befd3fc4c
06f70d134a3839677caa0d246
600b15c9dc53ef7a4551b8a90
a9c8af631c5361e8e1e1b8d9d
4b4b0aca3bbf15d35dd7d1050
f77024a07ee0dcee358dc1f5e
408c007b9d771718263b536e1
2de08e01684805a189224db75
c3838c034ae22d21f27e5d040
b15e9b0bab6ef6a56225a5983
251809396beb9d24b384f5fe8
cec6d262803311152db31b723
95d271ffdfe9df5861eefbaa4
7c11f3401530790b9ef510e55
e363390e2829097e7762bddc4
7ef34c69d9b8e38d72c6db29f
309a84490a7e387aaff1817ca
c214af2927c683954894365df
9f70859880b7ffa4b28265dbb
608e2f2f9e38025d92a1a4f03
c457a54d2da30a4a517edf14c
8670acbded737b1d2febdd954
99899b74b6469e366122b658c
3066408f5b4e86ef84bdb3fb9
010715f4955f66da3402bfa7b
fa01675690435b914631b46e1
2c5e234c5868799f31a6c983c
8345da31809ab2d9714a01d05
7b4e0e507dd0a8b6d7170a265
5aa71aded9fe7afa9a93a98c5
3714fb9f061398d4bb6af909d
165dd0af233cce64cefec12ed
849dda54070b868b50f356068
fe5f6e408eda6e9d429fa34ed
cd13f8da95c5b92b16d9d2781
65d0f69b41ea996ae2f8783a5
5742caf7a922eb3aaa270df30
f381ac4b84f3315e9163f169e
8c2afa1ab32b6fe402bf97ba3
a9f431efe6fc98aa64dbecbc2
8f0746e4e9529326d087f828b
bfc3cbea4d7f5c4495a14fc49
e4bf2d1468c6482570612360e
f1c7238766acdb7f199049487
60ae8a1ffd6784f7bbbc7b437
30afd67f207de6e893f7c9f42
dfa151daccb0e8d64d100f719
07be6a7d4aab21ccd9942401b
73ca1a54fcc40f7a46f46afbd
94ed2888fb93cb65d819d9d52
b7317773c6a15aa0bdf25fa01
edbb7f20f7523d9d941f3ebce
99a3c204b9f2036d3c38342bb
a0585424b8ab2ffcabee299d5
64e669fe2490522451cf10f85
18b8be34d4c560cda5280a103
9524d1f024b3c9864a3fccf75
0e7e94e7974894c98442241bc
4a17cc5e3d2baabaa338f592e
b070eaf38f390516f5cf61aa7
cc7832ea327b7426d8d2b8c2b
0df0a1d4833ebbb5d463c56bf
1bb610a8bb4e241996c9c756a
34ac2fdeb4b88fe6321a1d9c3
f0b20f8e79090dcb65195524c
307252efdd2b833228e0c301f
3908e63b405501782e629ac0b
29e66717adf14fb30c626103d
c8abd48af5f9332b322dffad0
80cd4e162bc7e8fb3a756b48c
825c00cec2294061eb328dd97
106205a2e24609652d149bc17
f1f896657fbc6f6287e7dee20
0fbd16ade658e24d69f76a225
4ab3b5eeeda86fa81afba796a
11d34f3d2ffb61d55da560ddb
013d6151bad187906fcc579a4
4509279a28f34bcf5327dd4c0
3c0eb47b3f9dc5a2f794bb9ad
1e6506906f23542c889330836
e7b1c5012390f3c7c48def9f3
d86caa695cb5fa1e0a2ead4cc

但我无法确认这是否能保证我的生产环境达到 99% 的成功率。

如果有人能给我建议,或者给我一个例子,我将不胜感激!

这应该是评论,但有点长。

您对 "unique" 和 "all the time" 的使用有些混淆。令牌要么是唯一的,要么不是。单独使用随机数生成器来创建令牌不足以保证唯一性——随机数生成器的全部意义在于你不知道下一个要生成的值是什么——这意味着你也不知道下一个号码不会与前一个号码相同。 OTOH,使用 random_bytes() 或 openssl_random_pseudo_bytes() 生成一个“99% 始终唯一”的令牌似乎是一个巨大的矫枉过正。

为了弄清楚这可能有多独特,我们需要知道在任何时候人口中有多少令牌会被考虑(或者能够根据预期的创建率和 TTL 来计算) ).

您使用大数字意味着您有充分的理由不使用最简单和最明显的唯一标识符 - 即递增整数。因此,拒绝猜测现有标识符显然对实施至关重要 - 但您又一次没有告诉我们任何相关信息。

将您的 post 的标题粘贴到 Google 中会出现您的 post 作为最佳结果 - 紧随其后的是 PHP's uniqid() 函数 - 但对于某些人来说原因是您没有找到 uniqid() 或出于某种原因拒绝了它。

您 post 的标题也是一个矛盾修辞法 - 为了定义一组无限的标识符,标识符的长度必须是无限的。

it needs to be able to generate hundreds, millions of "hashes"

...并且您希望在 Zend Framework 中将其全部 运行? - 哈哈。

But I cannot confirm that this does guarantee me a 99% success rate for my production environment.

为什么不呢?你在这里有足够的信息来确认按位熵是均匀分布的,并且应该知道生产环境的计划容量。剩下的就是基础算术了。

函数 random_bytes 生成加密安全随机字节

对于 openssl_random_pseudo_bytes 添加 crypto_strong 参数以确保所使用的算法具有很强的加密强度。

由于您的要求只有 99% 的唯一加密安全随机字节将满足您的要求。

我们大约有 8x10⁹ 人。想象一下,我们所有人在一年中每秒访问您的网站一次,需要一个唯一标识符。您需要大约 2,52288×10²³ 标识符。如果您认为您的站点将投入生产大约 1000 年,并且人口会增加 1000 倍,那么您需要大约 10²⁹ 标识符;所以一个 32 字节的自动递增字符串就足够了。添加伪随机 32 字节字符串作为后缀以获得安全的 64 字节标识符。做一点加上你可以散列标识符来创建令牌。

然后很容易写一个函数来获取它们。

2017/04/13 编辑 一个小样本: 您首先需要的是一个伪随机强密钥生成器。我将post我目前使用的功能:

<?php
function pseudoRandomBytes($count = 32){
  static $random_state, $bytes, $has_openssl, $has_hash;
  $missing_bytes = $count - strlen($bytes);
  if ($missing_bytes > 0) {
  // If you are using a Php version before 5.3.4 avoid using
  // openssl_random_pseudo_bytes()
    if (!isset($has_openssl)) {
      $has_openssl = version_compare(PHP_VERSION, '5.3.4', '>=') 
                   && function_exists('openssl_random_pseudo_bytes');
    }
    // to get entropy
    if ($has_openssl) {
      $bytes .= openssl_random_pseudo_bytes($missing_bytes);
    } elseif ($fh = @fopen('/dev/urandom', 'rb')) {
      // avoiding openssl_random_pseudo_bytes()
      // you find entropy at /dev/urandom usually available in most
      // *nix systems
      $bytes .= fread($fh, max(4096, $missing_bytes));
      fclose($fh);
    }
    // If it fails you must create enough entropy
    if (strlen($bytes) < $count) {
      // Initialize on the first call. The contents of $_SERVER 
      // includes a mix of user-specific and system information 
      // that varies a little with each page.
      if (!isset($random_state)) {
        $random_state = print_r($_SERVER, TRUE);
        if (function_exists('getmypid')) {
          // Further initialize with the somewhat random PHP process ID.
          $random_state .= getmypid();
        }
        // hash() is only available in PHP 5.1.2+ or via PECL.
        $has_hash = function_exists('hash') 
                  && in_array('sha256', hash_algos());
        $bytes = '';
      }
      if ($has_hash) {
        do {
          $random_state = hash('sha256', microtime() . mt_rand() . 
          $random_state);
          $bytes .= hash('sha256', mt_rand() . $random_state, TRUE);
        } while (strlen($bytes) < $count);
      } else {
        do {
          $random_state = md5(microtime() . mt_rand() . $random_state);
          $bytes .= pack("H*", md5(mt_rand() . $random_state));
        } while (strlen($bytes) < $count);
      }
    }
  }
  $output = substr($bytes, 0, $count);
  $bytes = substr($bytes, $count);
  return $output;
}

一旦你有了那个函数,你就需要一个函数来创建你的随机密钥:

<?php
function pseudo_random_key($byte_count = 32) {
  return base64_encode(pseudoRandomBytes($byte_count));
}

因为随机并不意味着唯一!您需要按照我的建议合并一个唯一的 32 字节前缀。由于大数函数很耗时,我将使用一个块数学函数,该函数使用我想不时使用 cron 函数生成并存储在环境 DB 变量中的前缀和自动增量索引也 db-stored

<?php
function uniqueChunkMathKeysPrefix(){
  // a call to read your db for prefix
  // I suppose you have an environment string-keyed table
  // and a couple of dbfunction to read and write data to it
  $last18bytesPrefix = dbReadEnvVariable('unique_prefix');
  // Also you store your current index wich returns to 0 once you get
  // a 99999999999999 value
  $lastuniqueindex = dbReadEnvVariable('last_unique_keys_index');
  if ($lastuniqueindex < 99999999999999){
    $currentuniqueindex = $lastuniqueindex + 1;
    $curret18bytesPrefix = $last18bytesPrefix;
  }else{
    $currentuniqueindex = 0;
    $curret18bytesPrefix = dbReadEnvVariable('next_unique_prefix');
    // flag your db variables to notify cron to create a new next prefix
    dbStoreEnvVariable('next_unique_prefix', 0);
    dbStoreEnvVariable('unique_prefix', $curret18bytesPrefix);
    // you have the time needed to have site visits and create new 
    // 99999999999999 keys as a while to run your cron to adjust your 
    // next prefix
  }
  // store your current index
  dbStoreEnvVariable('last_unique_keys_index', $currentuniqueindex);
  // Finally you create the unique index prefix part
  $uniqueindexchunk = substr('00000000000000'.$currentuniqueindex, -14);
  // return the output
  return $curret18bytesPrefix.$uniqueindexchunk;
}

现在你可以为 unique pseudo-random 64 bytes uniquekeys 写一个函数

<?php
function createUniquePseudoRandomKey(){
  $newkey = uniqueChunkMathKeysPrefix() . pseudo_random_key(32);
  // to beautify the output make a dummie call
  // masking the 0s ties
  return md5($newkey);
}