如何生成随机 SHA1 哈希以用作 node.js 中的 ID?
Posted
技术标签:
【中文标题】如何生成随机 SHA1 哈希以用作 node.js 中的 ID?【英文标题】:How to generate random SHA1 hash to use as ID in node.js? 【发布时间】:2012-03-13 13:17:37 【问题描述】:我正在使用这一行为 node.js 生成一个 sha1 id:
crypto.createHash('sha1').digest('hex');
问题是它每次都返回相同的 id。
是否可以让它每次生成一个随机 id 以便我可以将其用作数据库文档 id?
【问题讨论】:
不要使用 sha1。它不再被认为是安全的(防碰撞)。这就是为什么naomik's answer 更好。 @Niels Abildgaard 在生成后检查重复项是微不足道的,并且冲突的风险不一定相关,随着数据库大小的增加,无论您使用什么,都需要检查重复项。 “安全”与基数无关。 @mckenzm 这些都不一定正确:您不知道 ID 的用途、带有 ID 的项目是如何创建的、哪些假设适用于具体应用程序(因此如果耐碰撞性很重要)。而且您无论如何都不需要检查重复项,这就是防冲突 ID 的全部意义所在,也是它们特别用于大型和分散式应用程序的原因。 @ Niels Abildgaard,OP 只想要一个平衡的合成密钥。从稀薄的空气中。通常我们会反转时间戳或散列当前的行数。它只需要在表中分布良好且唯一,密钥将强制执行。然后将需要再进行一次。防撞性很好,但绝不是绝对的。 【参考方案1】:243,583,606,221,817,150,598,111,409 倍的熵
我建议使用crypto.randomBytes。这不是sha1
,但出于 id 的目的,它更快,而且就像“随机”一样。
var id = crypto.randomBytes(20).toString('hex');
//=> f26d60305dae929ef8640a75e70dd78ab809cfe9
生成的字符串将是您生成的随机字节的两倍;编码为十六进制的每个字节是 2 个字符。 20 个字节将是 40 个十六进制字符。
使用 20 个字节,我们有 256^20
或 1,461,501,637,330,902,918,203,684,832,716,283,019,655,932,542,976 唯一的输出值。这与 SHA1 的 160 位(20 字节)可能的输出相同。
知道了这一点,shasum
我们的随机字节对我们来说并没有什么意义。这就像掷骰子两次但只接受第二次掷骰;无论如何,每卷都有 6 种可能的结果,所以第一卷就足够了。
为什么这样更好?
要了解为什么这样做更好,我们首先必须了解散列函数的工作原理。如果给定相同的输入,散列函数(包括 SHA1)将始终生成相同的输出。
假设我们想要生成 ID,但我们的随机输入是通过抛硬币生成的。我们有"heads"
或"tails"
% echo -n "heads" | shasum
c25dda249cdece9d908cc33adcd16aa05e20290f -
% echo -n "tails" | shasum
71ac9eed6a76a285ae035fe84a251d56ae9485a4 -
如果"heads"
再次出现,SHA1 输出将与第一次相同
% echo -n "heads" | shasum
c25dda249cdece9d908cc33adcd16aa05e20290f -
好的,所以抛硬币不是一个很好的随机 ID 生成器,因为我们只有 2 个可能的输出。
如果我们使用标准的 6 面模具,我们有 6 个可能的输入。猜猜有多少可能的 SHA1 输出? 6!
input => (sha1) => output
1 => 356a192b7913b04c54574d18c28d46e6395428ab
2 => da4b9237bacccdf19c0760cab7aec4a8359010b0
3 => 77de68daecd823babbb58edb1c8e14d7106e83bb
4 => 1b6453892473a467d07372d45eb05abc2031647a
5 => ac3478d69a3c81fa62e60f5c3696165a4e5e6ac4
6 => c1dfd96eea8cc2b62785275bca38ac261256e278
我们很容易自欺欺人地认为函数的输出看起来非常随机,它非常随机。
我们都同意抛硬币或 6 面骰子会产生错误的随机 ID 生成器,因为我们可能的 SHA1 结果(我们用于 ID 的值)很少。但是,如果我们使用具有更多输出的东西呢?像毫秒的时间戳?还是 javascript 的 Math.random
?甚至是这两者的组合?!
让我们计算一下我们将获得多少个唯一 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*1000
或60000
唯一身份。
Math.random
的唯一性
现在,当使用Math.random
时,由于 JavaScript 表示 64 位浮点数的方式,您将得到一个长度介于 13 到 24 个字符之间的数字。更长的结果意味着更多的数字,这意味着更多的熵。首先,我们需要找出最可能的长度。
下面的脚本将确定最可能的长度。为此,我们生成 100 万个随机数并根据每个数字的 .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 个唯一身份。
那么我们可以生成多少个随机输入?
好的,我们计算了毫秒时间戳和Math.random
的结果数
100,000,000,000,000,001 (Math.random)
* 60,000 (timestamp)
-----------------------------
6,000,000,000,000,000,060,000
这是一个 6,000,000,000,000,000,060,000 面的模具。或者,为了使这个数字更容易被人类消化,它大致与
相同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 来充分发挥它的潜力。那么我们用了多少?
node> 6000000000000000060000 / Math.pow(256,20) * 100
毫秒时间戳和 Math.random 仅使用 SHA1 的 160 位潜力的 4.11e-27%!
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
和零频率的说明
如果您对 +1
感到疑惑,Math.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
所以事实证明,0
并不难获得。记录100 values后,平均值为
3,164,854,823 随机数中的 1 是 0
酷!需要进行更多研究才能知道该数字是否与 v8 的 Math.random
实现的均匀分布相当
【讨论】:
crypto.randomBytes
绝对是要走的路^^
A millisecond timestamp and Math.random uses only 4.11e-27 percent of SHA1's 160-bit potential!
实际上不是4.11e-25 percent
吗? 6e21 / 256**20 * 100
是 4.1053665947016126e-25
。这也意味着0.00000000000000000000000000411%
实际上应该是0.000000000000000000000000411%
,等等。【参考方案2】:
看看这里:How do I use node.js Crypto to create a HMAC-SHA1 hash? 我会创建当前时间戳的哈希 + 一个随机数以确保哈希唯一性:
var current_date = (new Date()).valueOf().toString();
var random = Math.random().toString();
crypto.createHash('sha1').update(current_date + random).digest('hex');
【讨论】:
要获得更好的方法,请参阅下面@naomik 的答案。 这也是一个很好的答案,Gabi,只是快了一点点,大约 15%。两者都做得很好!我实际上喜欢在 salt 中看到 Date(),它让开发人员很容易相信这将是除了最疯狂的并行计算情况之外的所有情况下的独特价值。我知道它的愚蠢和 randomBytes(20) 将是独一无二的,但这只是我们可以拥有的信心,因为我们可能不熟悉另一个库的随机生成的内部结构。 @Dmitri R117 加盐主键有什么好处?不需要保证唯一值,因为您可以在插入时检测到重复项。一些商店预先分配块。对于更平坦的索引或相同大小的分区,分布是同质的更为重要。【参考方案3】:在浏览器中也可以!
编辑:这并不真正适合我之前回答的流程。对于可能希望在浏览器中执行此操作的人,我将其留在这里作为第二个答案。
如果您愿意,可以在现代浏览器中执行此客户端
// 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 = 40)
var arr = new Uint8Array(len / 2);
window.crypto.getRandomValues(arr);
return Array.from(arr, byteToHex).join("");
console.log(generateId())
// "1e6ef8d5c851a3b5c5ad78f96dd086e4a77da800"
console.log(generateId(20))
// "d2180620d8f781178840"
浏览器要求
Browser Minimum Version
--------------------------
Chrome 11.0
Firefox 21.0
IE 11.0
Opera 15.0
Safari 5.1
【讨论】:
Number.toString(radix)
并不总是保证 2 位数的值(例如:(5).toString(16)
= "5",而不是 "05")。这无关紧要,除非您依赖最终输出的长度正好为 len
个字符。在这种情况下,您可以在 map 函数中使用 return ('0'+n.toString(16)).slice(-2);
。
很棒的代码,谢谢。只是想补充一点:如果您要将它用于 id
属性的值,请确保 ID 以字母开头:[A-Za-z]。
很好的答案(和 cmets) - 非常感谢您在答案中包含浏览器要求!
浏览器要求不正确。 Array.from() 在 IE11 中不支持。
在给出这个答案时取自维基。您可以根据需要编辑此答案,但谁真正关心 IE?如果你想支持它,无论如何你都必须填充一半的 JavaScript...【参考方案4】:
使用crypto
是一个很好的方法,因为它是原生且稳定的模块,
但在某些情况下,如果您想创建一个非常强大且安全的哈希,您可以使用bcrypt
。我将它用于密码,它有很多用于散列、创建盐和比较密码的技术。
技术 1(在单独的函数调用上生成盐和散列)
const salt = bcrypt.genSaltSync(saltRounds);
const hash = bcrypt.hashSync(myPlaintextPassword, salt);
技术 2(自动生成盐和哈希):
const hash = bcrypt.hashSync(myPlaintextPassword, saltRounds);
有关更多示例,您可以在此处查看:https://www.npmjs.com/package/bcrypt
【讨论】:
【参考方案5】:如果要获取唯一标识符,则应使用 UUID(通用唯一标识符)/GUID(全局唯一标识符)。
对于任何大小的输入,散列应该是确定性的、唯一的和固定长度的。所以无论你运行多少次哈希函数,如果你使用相同的输入,输出都是一样的。
UUID 是唯一且随机生成的! 有一个名为 'uuid' 的包,你可以通过 npm 安装它
npm 安装 uuid
& 在你的代码中导入模块
const v4:uuidv4 = require('uuid');
// 调用方法 uuidv4 或任何您在导入和记录或存储或分配它时命名的方法。该方法以字符串的形式返回一个 UUID。
console.log(uuidv4()); // 示例输出:'59594fc8-6a35-4f50-a966-4d735d8402ea'
这是 npm 链接(如果需要): https://www.npmjs.com/package/uuid
【讨论】:
以上是关于如何生成随机 SHA1 哈希以用作 node.js 中的 ID?的主要内容,如果未能解决你的问题,请参考以下文章