如何在node.js中生成随机的SHA1哈希值作为ID?

我正在使用这一行来为node.js生成一个sha1标识符:

crypto.createHash('sha1').digest('hex'); 

问题是每次都返回相同的ID。

是否有可能让它每次生成一个随机的ID,所以我可以使用它作为数据库文档ID?

看看这里: 我如何使用node.jsencryption来创build一个HMAC-SHA1哈希? 我会创build一个散列的当前时间戳+一个随机数,以确保散列唯一性:

 var current_date = (new Date()).valueOf().toString(); var random = Math.random().toString(); crypto.createHash('sha1').update(current_date + random).digest('hex'); 

更多的熵243,583,606,221,817,150,598,111,409x

我build议使用crypto.randomBytes 。 这不是sha1 ,但是为了id的目的,它更快,就像“随机”一样。

 var id = crypto.randomBytes(20).toString('hex'); //=> bb5dc8842ca31d4603d6aa11448d1654 

结果string将是您生成的随机字节的两倍。 每个字节编码为hex是2个字符。 20个字节将是hex的40个字符。

使用20个字节,我们有256^201,461,501,637,330,902,918,203,684,832,716,283,019,655,932,542,976个唯一的输出值。 这 SHA1的160位(20字节)可能的输出相同。

知道这一点,我们shasum我们的随机字节是没有意义的。 这就像滚动模具两次,但只接受第二轮; 不pipe怎么样,每个滚动你都有6个可能的结果,所以第一个滚动就足够了。


为什么这更好?

要理解为什么这样更好,我们首先必须了解哈希函数是如何工作的。 散列函数(包括SHA1)将始终生成相同的输出,如果给定相同的input。

假设我们想要生成ID,但是我们的随机input是由掷硬币产生的。 我们有"heads""tails"

 % echo -n "heads" | shasum c25dda249cdece9d908cc33adcd16aa05e20290f - % echo -n "tails" | shasum 71ac9eed6a76a285ae035fe84a251d56ae9485a4 - 

如果"heads"重新出现,SHA1输出将与第一次相同

 % echo -n "heads" | shasum c25dda249cdece9d908cc33adcd16aa05e20290f - 

好的,所以抛硬币并不是一个很好的随机ID生成器,因为我们只有2个可能的输出。

如果我们使用标准的6面模具,我们有6个可能的input。 猜猜有多less可能的SHA1输出? 6!

 input => (sha1) => output 1 => 356a192b7913b04c54574d18c28d46e6395428ab 2 => da4b9237bacccdf19c0760cab7aec4a8359010b0 3 => 77de68daecd823babbb58edb1c8e14d7106e83bb 4 => 1b6453892473a467d07372d45eb05abc2031647a 5 => ac3478d69a3c81fa62e60f5c3696165a4e5e6ac4 6 => c1dfd96eea8cc2b62785275bca38ac261256e278 

只是因为我们的函数输出看起来非常随机,而且非常随机,所以很容易让我们自欺欺人。

我们都同意掷硬币或者六面模具会产生一个不好的随机id生成器,因为我们可能的SHA1结果(我们用于ID的值)很less。 但是如果我们用一些有更多输出的东西呢? 像一个毫秒的时间戳? 或JavaScript的Math.random ? 甚至是这两者的结合

让我们计算一下我们会得到多less个独特的ID


时间戳的唯一性,以毫秒为单位

当使用(new Date()).valueOf().toString() ,你会得到一个13个字符的数字(例如1375369309741 )。 然而,由于这是一个顺序更新的数字(每毫秒一次),输出几乎总是相同的。 让我们来看看

 for (var i=0; i<10; i++) { console.log((new Date()).valueOf().toString()); } console.log("OMG so not random"); // 1375369431838 // 1375369431839 // 1375369431839 // 1375369431839 // 1375369431839 // 1375369431839 // 1375369431839 // 1375369431839 // 1375369431840 // 1375369431840 // OMG so not random 

公平地说,为了比较的目的, 在一个给定的分钟 (一个慷慨的操作执行时间),你将有60*100060000唯一。


Math.random的独特性

现在,当使用Math.random ,由于JavaScript代表64位浮点数的方式,您将得到一个长度在13到24个字符之间的数字。 更长的结果意味着更多的数字,这意味着更多的熵。 首先,我们需要找出最可能的长度。

下面的脚本将确定最可能的长度。 我们通过生成一百万个随机数字并基于每个数字的.length递增计数器来实现这一点。

 // get distribution var counts = [], rand, len; for (var i=0; i<1000000; i++) { rand = Math.random(); len = String(rand).length; if (counts[len] === undefined) counts[len] = 0; counts[len] += 1; } // calculate % frequency var freq = counts.map(function(n) { return n/1000000 *100 }); 

通过将每个计数器除以100万,我们得到从Math.random返回的数字长度的概率。

 len frequency(%) ------------------ 13 0.0004 14 0.0066 15 0.0654 16 0.6768 17 6.6703 18 61.133 <- highest probability 19 28.089 <- second highest probability 20 3.0287 21 0.2989 22 0.0262 23 0.0040 24 0.0004 

所以,即使这不是完全正确的,让我们慷慨地说,你得到一个19个字符的随机输出; 0.1234567890123456789 。 第一个字符将始终是0. ,所以我们只能得到17个随机字符。 这给我们留下了10^17 +1 (可能为0 ;见下面的注释)或100,000,000,000,000,001独特。


那么我们可以产生多less个随机input?

好的,我们计算了毫秒时间戳和Math.random的结果数量

  100,000,000,000,000,001 (Math.random) * 60,000 (timestamp) ----------------------------- 6,000,000,000,000,000,060,000 

这是一个单一的6000000000000000000双面模具。 或者,为了使这个数字更人性易消化,这个数字与数字大致相同

 input outputs ------------------------------------------------------------------------------ ( 1×) 6,000,000,000,000,000,060,000-sided die 6,000,000,000,000,000,060,000 (28×) 6-sided die 6,140,942,214,464,815,497,21 (72×) 2-sided coins 4,722,366,482,869,645,213,696 

听起来不错,对吧? 那么,我们来找出…

SHA1产生一个20字节的值,可能有256 ^ 20个结果。 所以我们真的没有使用SHA1来充分发挥它的潜力。 那么我们使用多less?

 node> 6000000000000000060000 / Math.pow(256,20) * 100 

毫秒时间戳和Math.random只使用4.11e-27%的SHA1的160位潜力!

 generator sha1 potential used ----------------------------------------------------------------------------- crypto.randomBytes(20) 100% Date() + Math.random() 0.00000000000000000000000000411% 6-sided die 0.000000000000000000000000000000000000000000000411% A coin 0.000000000000000000000000000000000000000000000137% 

神圣的猫,男人! 看看所有这些零。 那么crypto.randomBytes(20)有多好? 更好的是243,583,606,221,817,150,598,111,409


关于零的+1和频率的注意事项

如果您想知道+1Math.random可能会返回0 ,这意味着我们必须考虑另外1个可能的唯一结果。

根据下面的讨论,我对一个0会出现的频率感到好奇。 这里有一个脚本, random_zero.js ,我用来获取一些数据

 #!/usr/bin/env node var count = 0; while (Math.random() !== 0) count++; console.log(count); 

然后,我运行它在4个线程(我有一个4核心处理器),将输出附加到一个文件

 $ yes | xargs -n 1 -P 4 node random_zero.js >> zeroes.txt 

所以事实certificate,一个0不是很难得到。 logging了100个值之后,平均值为

3,164,854,823个随机中是1是0

凉! 需要更多的研究来了解这个数字是否与v8的Math.random实现的统一分布一致

在浏览器中也是这样!

编辑:这不适合我以前的答案的stream程。 我将把它留在这里作为第二个答案,可能是在浏览器中寻找的人。

如果你愿意,你可以在现代浏览器中做这个客户端

 // str byteToHex(uint8 byte) // converts a single byte to a hex string function byteToHex(byte) { return ('0' + byte.toString(16)).slice(-2); } // str generateId(int len); // len - must be an even number (default: 40) function generateId(len) { var arr = new Uint8Array((len || 40) / 2); window.crypto.getRandomValues(arr); return [].map.call(arr, byteToHex).join(""); } 

好的,我们来看看吧!

 generateId(); // "1e6ef8d5c851a3b5c5ad78f96dd086e4a77da800" generateId(20); // "d2180620d8f781178840" 

浏览器要求

 Browser Minimum Version -------------------------- Chrome 11.0 Firefox 21.0 IE 11.0 Opera 15.0 Safari 5.1