Nanoid or UUid
1.What is UUid?
通用唯一识别码(英语:Universally Unique Identifier,缩写:UUID)是用于计算机体系中以识别信息的一个128位标识符。
2. UUID生成的几种方法
2.1. UUID Version 1:基于时间的UUID
基于时间的UUID通过计算当前时间戳、随机数和机器MAC地址得到。由于在算法中使用了MAC地址,这个版本的UUID可以保证在全球范围的唯一性。但与此同时,使用MAC地址会带来安全性问题,这就是这个版本UUID受到批评的地方。如果应用只是在局域网中使用,也可以使用退化的算法,以IP地址来代替MAC地址--Java的UUID往往是这样实现的(当然也考虑了获取MAC的难度)。但由于时间因素的顺序为时间低位在前,高位在后,不适合做主键,可以组合。
2.2. UUID Version 2:基于名字的UUID(MD5)
基于名字的UUID通过计算名字和名字空间的MD5散列值得到。这个版本的UUID保证了:相同名字空间中不同名字生成的UUID的唯一性;不同名字空间中的UUID的唯一性;相同名字空间中相同名字的UUID重复生成是相同的。
2.3. UUID Version 3:随机UUID
根据随机数,或者伪随机数生成UUID。这种UUID产生重复的概率是可以计算出来的,但随机的东西就像是买彩票:你指望它发财是不可能的,但狗屎运通常会在不经意中到来。
2.4. UUID Version 4:基于名字的UUID(SHA1)
和版本3的UUID算法类似,只是散列值计算使用SHA1(Secure Hash Algorithm 1)算法。
算法一
function uuid() {
var s = [];
var hexDigits = "0123456789abcdef";
for (var i = 0; i < 36; i++) {
s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
}
s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
s[8] = s[13] = s[18] = s[23] = "-";
var uuid = s.join("");
return uuid;
}
算法二
function uuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
}
算法三
function uuid() {
function S4() {
return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
}
return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
}
算法四
function uuid(len, radix) {
var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
var uuid = [], i;
radix = radix || chars.length;
if (len) {
// Compact form
for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random()*radix];
} else {
// rfc4122, version 4 form
var r;
// rfc4122 requires these characters
uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
uuid[14] = '4';
// Fill in random data. At i==19 set the high bits of clock sequence as
// per rfc4122, sec. 4.1.5
for (i = 0; i < 36; i++) {
if (!uuid[i]) {
r = 0 | Math.random()*16;
uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
}
}
}
return uuid.join('');
}
对于第四中算法可以指定长度和基数。比如
// 8 character ID (base=2)
uuid(8, 2) // "01001010"
// 8 character ID (base=10)
uuid(8, 10) // "47473046"
// 8 character ID (base=16)
uuid(8, 16) // "098F4D35"
3.Nanoid的安全性
nanoid的README里介绍了 Math.random + 字母表 的实现方式是不安全的,它给出的论据是有部分字母的出现概率低于其他字母:
其实吧,关于这点,我不太信,但是不知道它是怎么验证的。我简单的自己写了个1亿次的循环,发现出现概率还是比较平均的,没有显著差异5%。或许,它迭代次数更多并且时间更长?但我觉得次数更多应该更趋于平均才对啊~
其实吧,关于这点,我不太信,但是不知道它是怎么验证的。或许,它迭代次数更多并且时间更长?
经大神提醒,我也从nanoid的仓库看到了demo代码,我明白了这里为什么分布不均,是因为如果采用随机数 % 字母表长度的算法提取字母,就会出现这样分配不均的情况,项目里的例子是0~255 % 26,正好差4个字母是整数倍,也就必然造成了最后4个字母分布不均的情况。也就是它为什么不采用这种算法提取字母的原因。
不过这点我不深究了,它既然敢说,我也敢信,因为我也知道 Math.random 是伪随机,在一定情况下确实可以预测出来:
node --random-seed=42
Welcome to Node.js v14.17.3.
Type ".help" for more information.
> Math.random()
0.5254990606499601
> Math.random()
0.963056226312738
3.1. Crypto.getRandomValues
var array = new Uint32Array(10);
window.crypto.getRandomValues(array);
就是传入一个数组,数组长度就是需要多少个随机数,而每一个子项根据你传入的类型决定了随机数的范围是0XXX。比如这里是uint32,那就是范围是0232-1。
很简单的用法,那看看nanoid是怎么使用的:代码
let nanoid = (size = 21) => {
let id = ''
let bytes = crypto.getRandomValues(new Uint8Array(size))
// A compact alternative for `for (var i = 0; i < step; i++)`.
while (size--) {
// It is incorrect to use bytes exceeding the alphabet size.
// The following mask reduces the random byte in the 0-255 value
// range to the 0-63 value range. Therefore, adding hacks, such
// as empty string fallback or magic numbers, is unneccessary because
// the bitmask trims bytes down to the alphabet size.
let byte = bytes[size] & 63
if (byte < 36) {
// `0-9a-z`
id += byte.toString(36)
} else if (byte < 62) {
// `A-Z`
id += (byte - 26).toString(36).toUpperCase()
} else if (byte < 63) {
id += '_'
} else {
id += '-'
}
}
return id
}
nanoid总共生成21位字符串,每一位随机数是uint8型,即随机0~255。
但是字母表不可能有256个字符阿?那nanoid如何处理呢?
所以,nanoid的字母表是:A-Za-z0-9_-。即26 + 26 + 10 + 2 = 64,很关键,正好256可以整除。也是因为如此,它才故意增加了-_这两个字符凑字母表~~有点base64那味了
3.2. 自定义字母表
默认逻辑很好理解,那如果我传入的自定义字母表不能被整除,那如何保证各字符的出现概率相等呢?
好家伙,看不懂:代码
let customRandom = (alphabet, size, getRandom) => {
// First, a bitmask is necessary to generate the ID. The bitmask makes bytes
// values closer to the alphabet size. The bitmask calculates the closest
// `2^31 - 1` number, which exceeds the alphabet size.
// For example, the bitmask for the alphabet size 30 is 31 (00011111).
// `Math.clz32` is not used, because it is not available in browsers.
let mask = (2 << (Math.log(alphabet.length - 1) / Math.LN2)) - 1
// Though, the bitmask solution is not perfect since the bytes exceeding
// the alphabet size are refused. Therefore, to reliably generate the ID,
// the random bytes redundancy has to be satisfied.
// Note: every hardware random generator call is performance expensive,
// because the system call for entropy collection takes a lot of time.
// So, to avoid additional system calls, extra bytes are requested in advance.
// Next, a step determines how many random bytes to generate.
// The number of random bytes gets decided upon the ID size, mask,
// alphabet size, and magic number 1.6 (using 1.6 peaks at performance
// according to benchmarks).
// `-~f => Math.ceil(f)` if f is a float
// `-~i => i + 1` if i is an integer
let step = -~((1.6 * mask * size) / alphabet.length)
return () => {
let id = ''
while (true) {
let bytes = getRandom(step)
// A compact alternative for `for (var i = 0; i < step; i++)`.
let j = step
while (j--) {
// Adding `|| ''` refuses a random byte that exceeds the alphabet size.
id += alphabet[bytes[j] & mask] || ''
if (id.length === size) return id
}
}
}
}
虽然嘴上说着看不懂,但身体还是很老实,还是耐着性子看了下来。。。
代码也不长,关键逻辑分三步。
1.第一步
let mask = (2 << (Math.log(alphabet.length - 1) / Math.LN2)) - 1
这是为了得出能表示字符表长度的最大数值(0b10左移N位再-1,二进制表示就是全1的)。比如文中提到的30,二进制表示是0b11110,那最大能包括它的是(0b10左移4位再-1)0b11111,也就是2^5-1,即31。
2.第二步
let step = -~((1.6 * mask * size) / alphabet.length)
这一步目的是为了生成迭代次数,至于为什么不直接是目标值size?是因为随机数有可能是超过字符表长度的,所以需要一点冗余容错长度。
至于为什么1.6倍这么多就够了,这可能是概率+性能原因吧,我也解释不了。。。
下面评论里说的更详细了些,我也是表述不清,其实是因为之前的分布不均原因,而随机数必然是0~255,那字母表长度如果是2^N,则可以被整除,所以这也是mask存在的原因。
3.第三步
id += alphabet[bytes[j] & mask] || ''
好吧,又是位操作,这步是保证随机出来的数不会超过mask,也就是随机数和mask与出来的结果,大概率是字符表长度范围内的。
看到这步其实可以发现一个风险,因为随机数使用的是uint8,所以如果字符表长度超过256,mask也就超过256。比如,mask是511,那实际运算就是511 & (0255的随机数),结果必然是0255,那字母表中超过256的字符永远不会随机到。
我觉得nanoid不会有这么明显的漏洞,所以我又看了下文档,果然是这么设计的,它也给出了提醒:
Alphabet must contain 256 symbols or less. Otherwise, the security of the internal generator algorithm is not guaranteed.
剩下的,就是当随机数大小超过字符表长度时,需要一个容错(第二步提到的)。
上文代码的这个容错就是为了当随机数超过字符表长度时候可以兼容下,最终,当达到输出id长度等于size要求时,循环终止。
4.Nanoid的性能
看完了nanoid的实现,我们理解这是一个更“安全”的实现方式。那么我们看看性能,nanoid的官方说明使用的是node环境,虽然也有一定道理,但本文主要专注于浏览器环境,所以还是将部分benchmark测试搬到了web端(不得不吐槽好多库如果没有webpack/rollup,没那么方便迁到web使用)。
nanoid 252,908 ops/sec
uuid v4 632,771 ops/sec
unsecure nanoid 2,452,412 ops/sec
可以看到,Crypto API的性能还是相对差一些的,不过同样是Math.random(),虽然nanoid短于uuid,但也快太多了。。。
除此之外顺便介绍一下Chrome官方出品的uuid方法,v92版本新加入特性——Crypto.randomUUID。非规范,MDN都查不到描述。
使用方式很简单:
window.crypto.randomUUID(); // 6c9735ea-2fa9-4240-b974-e38d1324010a
因为它的兼容性奇烂,所以其实是冲击不了现在的uuid以及nanoid的使用场景的,但我还是顺便一起测试了下性能:
natvie uuid 366,414 ops/sec
性能上快于nanoid,但是还是再次证明了Crypto API的开销还是大。整体性能也显著差于基于Math.random实现方式的生成id方法。
5.Nanoid使用
5.1.在项目目录下打开终端,下载安装nanoid库
npm i nanoid
或者,如果你安装了yarn可以使用:
yarn add nanoid
5.2.引入nanoid库
nanoid库中用分别暴露的方式暴露了一个函数nanoid
import {nanoid} from 'nanoid'
5.3.使用nanoid生成uuid
直接调用nanoid(),即可生成一个uuid
import React, { Component } from 'react'
import {nanoid} from 'nanoid'
import "./index.css"
export default class Header extends Component {
handleKeyUp = (event) => {
const {keyCode, target} = event;
// 判断是否是回车
if (keyCode !== 13) return
if (target.value.trim() === '') {
alert("输入不能为空")
return
}
const todoObj = {id:nanoid(),name:target.value,done:false}
this.props.addTodo(todoObj)
target.value = ''
}
render() {
return (
<div className="todo-header">
<input onKeyUp={this.handleKeyUp} type="text" placeholder="请输入你的任务名称,按回车键确认"/>
</div>
)
}
}
6.UUID使用
6.1安装uuid库🔍
npm install uuid --save
6.2引用
import { v4 as uuidv4 } from 'uuid';
uuidv4(); // ⇨ '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
// 函数封装 获取8位数uuid
getUId = () => {
return uuidv4().split('-')[0]; // ⇨ '9b1deb4d'
}
看完了,赶快尝试一下吧!!!