唯一识别码之UUID入门与实战

7,468 阅读11分钟

上一篇从身份证号是如何生成,来认识了中心机构下生成唯一标识的方法,这一篇来看看面对庞大的计算机世界,无中心机构时,那又如何生成唯一标识呢?


认识UUID

接下来从一个广为人知的模块UUID讲起,它似乎在每个语言里都有对应的实现,甚至在部分Unix系统直接提供了实现。

UUID是什么?

UUID的全称是Universally Unique Identifier,中文为通用唯一识别码。本身是由一组32位数的16进制数字所构成,故UUID理论上的总数为1632=2128,约等于3.4 x 1038。也就是说若每纳秒产生1兆个UUID,要花100亿年才会将所有UUID使用完,也就是说最多100亿年的时间,UUID将必定出现重复,不过100亿年地球是否存在也似乎不确定,暂时不必考虑那么长远。

UUID的表现形式

UUID也是需要像身份证号一样事先制定一些简单的规则进去的,它的标准型式包含32个16进制数字,以连字号分为五段,表现形式为8-4-4-4-12的32个字符,如下所示:

xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx

其中M与N都有特殊含义,M表示UUID版本,目前只有五个版本,即只会出现1,2,3,4,5,数字 N的一至三个最高有效位表示 UUID 变体,目前只会出现8,9,a,b四种情况。

UUID的版本进化史

一、基于时间和空间的UUID

第一个版本的uuid与身份证号的设计有一点类似,在寻找一个中心机构来解决唯一性的问题,在互联网世界里,时间可以变得更加精细,与宏观世界里相比它能精确到纳秒。而空间的控制却是一个难题,万维网全球化的前提下,很难找出一个类似于政府的机构来制定统一规范,所以只能从计算机出厂时所带的唯一编码(MAC地址)来用作空间上的标识。
理想情况下,每一台计算机都有唯一的MAC地址,每一台计算机在某一时刻执行一次生成UUID的操作,在全球内肯定是唯一的。类似于二维世界横纵线的交汇,如果横向代表时间,时间永远不会倒流,纵向代表机器,机器不会在同一时刻执行两次操作。

但是现实情况却并非是这样,也从横纵两个方向去看。

1.计算机对于时间虽然精度很高,但是分布在世界各个角落里的情况下,计算机并不会通过某一个中心点获取当前时间,而是根据机器内部自身来获取,那就会出现一个问题,计算机自身时钟有误后被校准出现时间相同后生成uuid的问题,不过一般可以忽略,时间是相对的概念,只要自身一直保持一个时钟,便不会出现问题。2.实际上,MAC地址并非完全唯一。首先出厂计算机的商家也不是全球一家,即使约定了规范也不能保证网卡制造商没有误差地为网卡分配唯一的 MAC地址。另外计算机在用户的手里,MAC地址在用户计算机上,用户要是了解计算机的原理构造,能不能主动的修改一下MAC地址呢?答案是可以的。3.同时执行生成UUID程序。当两个进程同时跑了一段生成UUID的代码时,它们所处的时间点一致,MAC地址也一致,这时候便也会出现生成相同UUID的情况。

以上从几个角度去看第一版本的UUID生成后的会出现不唯一的原因,但是上述情况出现还是很小概率的,所以基本目前来说最可靠能保证全球的唯一性的实现方法,也因为此,第一版本UUID在一些前唯一性场景还是非常常见。


使用示例

Nodejs版本

我翻阅了一下uuid这一版本的源码,虽然使用的人非常多,但是实际内部实现并没有取机器的MAC地址,由随机数拼接而成。

const uuidv1 = require('uuid').v1;
const logger = console.log;
logger('uuid v1版本:%s', uuidv1());
// uuid v1版本:10e10f40-bd02-11e9-b241-97aa7a999bec

python版本

在python自带的uuid模块中,确实获取了机器网卡的MAC地址。

import uuid;
uuid.uuid1();
# UUID('e852b72e-ba4d-11e9-8e8e-acde48001122')

ifconfig命令查看一下网卡MAC。

从上两个例子可以均可看出M位是1,N位在a,b,8,9内,都是符合UUID开始时所述的规范。最后的12位acde48001122正是我机器的网卡,一直保持不变的。

暴露MAC地址所产生的安全问题

这一版本的UUID比较大的一个问题就在于它的组成里含有用户的MAC地址,每台计算机绑定了一个用户,则MAC地址也对应了用户,这代表着MAC地址的暴露则造成了隐私问题与安全问题。

通过UUID抓获病毒制造者

1998年,由美国人David L. Smith运用Word的宏运算编写出的一个电脑病毒,其主要是通过邮件传播,邮件的标题通常为“这是给你的资料,不要让任何人看见”,一旦收件人打开邮件,病毒就会自动向用户通讯录的前50位好友复制发送同样的邮件。尽管这种病毒不会删除电脑系统文件,但它引发的大量电子邮件会阻塞电子邮件服务器,使之瘫痪,造成了相当大的危害,最终就是这位病毒制造者David L. Smith就是因为在脚本中使用的UUID中暴露了机器的MAC信息,最后在计算机信息中心配合下,确定其位置并缉拿归案。

二、基于第一版却更安全的DCE UUID

这一版本的uuid是基于第一版本的,首先它也是从时间加空间的角度来生成的,然后又在其之上由将内部实现稍作修改,达到安全的目的。在内部实现中,这一版本的UUID除了时钟序列的最低有效8 bits 被本地域号替换,并且时间戳的最低有效32 bits 由在指定本地域内有意义的整数标识符替换。
这一版本我找了Nodejs的uuid包,python自带的uuid等包都没有相关的实现v2版本,所以没有办法找到示例代码了,看来这一版本的UUID用的人也非常少。

三、基于MD5散列算法的UUID

这一版本的UUID与上面两个版本出发角度就不相同,我理解是在哈希算法角度出发,当你有相同的输入时,你就可以得到相同的UUID结果。其内部实现有两个概念, 名字空间和输入内容,在生成UUID时,先要确定命名空间,然后将命名空间和输入的值进行连接,最后用 MD5 散列函数进行运算完成。

默认的命名空间

nodejs中

// nodejs uuid源码中预定义的命名空间
generateUUID.DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
generateUUID.URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8';

python中:

#python中默认预定义的命名空间
import uuid
uuid.NAMESPACE_DNS  #UUID('6ba7b810-9dad-11d1-80b4-00c04fd430c8');
uuid.NAMESPACE_URL  #UUID('6ba7b811-9dad-11d1-80b4-00c04fd430c8');
uuid.NAMESPACE_X500 #UUID('6ba7b811-9dad-11d1-80b4-00c04fd430c8');
uuid.NAMESPACE_XX   #UUID('6ba7b811-9dad-11d1-80b4-00c04fd430c8');


版本特点:

1. 基于相同命名空间下,不同输入值的生成的UUID不同,并非完全不同,有一定几率相同。

2. 基于相同命名空间下,相同输入值的生成的UUID不同。

3. 基于不同命名空间生成的UUID一定不会相同,当然我理解这是不出现MD5碰撞的前提下。

4. 基于两个输入值的UUID相同,那么一定是来自相同的命名空间下的同一个输入值。


使用示例

Nodejs版本

const uuidv3 = require('uuid/v3');
const logger = console.log;
logger('uuid v3版本:%s', uuidv3('myString', uuidv3.DNS))
// 21fc48e5-63f0-3849-8b9d-838a012a5936

python版

import uuid
uuid.uuid3(uuid.NAMESPACE_DNS, "myString")
# UUID('21fc48e5-63f0-3849-8b9d-838a012a5936')

四、基于随机数的UUID

这个版本的UUID是使用最多的,它的本质是根据随机数或者伪随机数来生成UUID,最大的问题就是这种重复率的问题,这一类型的UUID的重复率是可以计算出来的,所以大型长期的网站还是不建议采用这个版本的,当用久了后重复的概率越来越大,遇到的问题将越来越多。

一个 比较不错的基于JavaScript的实现。

function uuidv4() {  
  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);  
  });
}


使用示例

Nodejs版本

const uuidv4 = require('uuid/v4');
const logger = console.log;
logger('uuid v4版本:%s', uuidv4())

python版本:

import uuid
uuid.uuid4()# UUID('1a9e40e2-3862-41d4-bd4e-0dd928e81055')

Nodejs的UUID v4版源码分析

nodejs的uuid包中,v4版本实现比较简单,大家也可以去翻阅查看。我这里删减一部分代码,将主干留下来讲解。

// randomBytes的官方定义:生成加密的强伪随机数据。size参数是一个数字,指示要生成的字节数。
// 这里生成16字节数强伪随机数,返回类型为buffer的数据。
var rng = require('crypto').randomBytes(16);
// 将byte生成uuid 的 string的工具函数
function bytesToUuid(buf) {}
// 主干代码
module.exports = function v4() { 
  var rnds = rng();  
  // 位运算符&:两个数值的个位分别相与,同时为1才得1,只要一个为0就为0。  
  // 位运算符|:两个位只要有一个为1,那么结果都为1。否则就为0 
  // 将UUID的M和N位进行处理,处理后M位为4,N为a,b,8,9内的任意值  
  rnds[6] = (rnds[6] & 0x0f) | 0x40;  
  rnds[8] = (rnds[8] & 0x3f) | 0x80; 
  return bytesToUuid(rnds);
}

五、基于SHA1散列算法的UUID

这个版本与第三版本的UUID类似,但使用的散列算法不同 ,它利用SHA1 代替了 MD5,其余和第三个版本一样,但是相比于第三步版 更加推荐使用这一版本。


SHA1和MD5的区别

首先它们两个都是散列函数,对于SHA1来说,长度小于2^64位的消息,则会产生一个160位的消息摘要,而MD5最显著和最重要的区别是它的摘要比SHA1摘要少32 位,它只产生出一个128位的消息摘要,如果使用强行破解技术,SHA-1相比于MD5有更大的强度。

在Nodejs的uuid的实现中,V5与V3实现唯一不一致的就是散列函数不同。

// v3版本
crypto.createHash('md5').update(bytes).digest();
// v5版本
crypto.createHash('sha1').update(bytes).digest();


使用示例

Nodejs版本

const uuidv5 = require('uuid/v5');
const logger = console.log;
logger('uuid v5版本:%s', uuidv5('hello.example.com', uuidv5.DNS))
// uuid v5版本:fdda765f-fc57-5604-a269-52a7df8164ec

python版本

import uuid
uuid.uuid5(uuid.NAMESPACE_DNS, "hello.example.com")
#UUID('fdda765f-fc57-5604-a269-52a7df8164ec')

References

[1] Nodejs的uuid:https://www.npmjs.com/package/uuid

[2] 维基百科:https://zh.wikipedia.org/wiki/通用标识码


如上内容均为自己总结,难免会有错误或者认识偏差,如有问题,希望大家留言指正,以免误人,若有什么问题请留言,会尽力回答之。如果对你有帮助不要忘了分享给你的朋友或者点击右下方的“在看”哦!也可以关注作者,查看历史文章并且关注最新动态,助你早日成为一名全栈工程师!