[译文]在Web上大规模生成UUID

英文版权归Matthieu Wipliez所有,中译文作者yangyang(aka davidkoree)。双语版可用于非商业传播,但须注明英文版作者、版权信息,以及中译文作者。翻译水平有限,请广大读者指正。

长文勿读(译注:原文「TL;DR」,表示too long, don't read,这里是作者自嘲,皮一下)。让每个浏览器来大规模生成通用唯一标识符,你觉得靠谱吗?在Teads,我们做了尝试,答案是肯定的,但有几点警告。本文介绍了我们所做的探索,以及在此过程中的发现。

为什么我们需要客户端唯一标识符

「唯一标识符」作为一种普遍需求,主要用于分析、营销、或广告相关领域,Web页面的第三方脚本和电子商务站点便是它的载体。

这些脚本几乎总是从CDN(内容交付网络)加载,在大规模使用的场景中,这样可以获得最佳的响应时间并减轻原始服务器的负载。

这意味着脚本无法即时生成。解决方法可能是(或曾经是)让CDN生成唯一标识符并将其存储在Cookie中。但用户隐私法规(例如欧洲的GDPR和ePrivacy条例或美国的CCPA)禁止设置Cookie,除非用户已明确表示同意。

识别广告的独特体验

作为一家在线广告公司,Teads收集并存储有关「广告体验」方方面面的数据。所谓「广告体验」,既包括用户访问网页并加载广告脚本时发生的所有事件,也包括从初始化广告播放器开始对广告服务器的请求和用户操作(例如点击)。要将一组事件归类为同一批次的体验,我们需要能够唯一地识别这种体验,并且从一开始即在调用广告服务器之前就必须进行识别。

到目前为止,广告服务器一直在生成唯一标识符,并将其作为广告响应的一部分进行发送。这是有问题的,因为响应之前的事件没有标识符,因此你需要交叉引用数据以查找属于同一批次的事件。服务器端生成的标识符几乎可以保证是唯一的,并且在触达生产系统之前,我们必须确保浏览器也可以生成通用唯一的标识符。

通用唯一标识符

UUID(「通用唯一标识符」,也被称为GUID,即「全局唯一标识符」)是一个拥有128比特位的值,它可由计算机独立生成,即不与其它计算机进行通信,并且其唯一性应当具有非常高的概率。字面上用以破折号分隔的十六进制数字序列来表示UUID。

以下是RFC 4122定义的「版本4 UUID」的示例:

UUID最初是作为分布式计算而设计的,它是网络计算系统(NCS)的一部分,已在许多情况下使用,充分地利用了其属性。在Windows上,UUID的使用很普遍,因为它们标识了所有COM类(CLSID)和接口,因此所有基于COM的Windows API和应用程序以及许多OS对象(例如用户,安全策略等)都使用UUID。

实际上,除了上面显示的RFC兼容变体和保留变体之外,可以指定的四个变体中,其他两个是1)NCS向后兼容(最高有效位是0,数字0到7) 2)Microsoft Corporation向后兼容(最高有效位是110,数字C和D)。

UUID的其他应用程序包括文件系统,例如GUID分区表(UEFI的一部分)中的文件系统,或可以将其代替传统整数用作记录主键的数据库。在在线广告的上下文中,它们经常用于唯一地标识在Web上查看广告的用户。例如,Interactive Advertising Bureau(IAB)建议将UUID用于IDFA(广告标识符)/ AAID(Android的Google广告ID),以唯一地标识移动用户。

选择你的版本

UUID版本1和2使用以下组合生成标识符——宿主计算机的MAC地址、100纳秒精度的当前时间UTC的时间戳,以及“时钟序列”,这种组合可以在100ns的时间内消除标识符的歧义,可以单调递增或随机。

每个带有网络控制器的设备都应该具有唯一的48位MAC地址,这使得不可能有两个设备生成相同的UUID。但是,这也是这些版本的弱点,因为这意味着此类UUID可用于以个人方式唯一标识用户。请注意,这是在用户设备而非服务器上生成UUID时出现的一个问题,例如MySQL使用UUID v1。

UUID版本3和5是通过对字符串进行哈希处理(对于v3使用MD5,对于v5使用SHA-1)产生的,并且由于哈希是确定性的,因此输出与输入一样唯一。如果你想将URL用作唯一标识符,这个方法可行。但它们不适合我们的场景。

最后,在版本4的情况下,除变体和版本以外的所有位都是随机的,总计为122个随机位。这保证了这些UUID不会携带任何个人身份信息。需要注意的是,若要受益于UUID在唯一性和不可预测性上的保证,则应使用加密安全的随机数生成器(CSRNG)。

让我们在浏览器中生成一个UUID

正如我们已经看到的,如果我们拥有CSRNG,则「版本4 UUID」是我们的最佳选择。这立即排除了经久不衰的Math.random,后者的实现取决于浏览器,并且不能保证密码使用的安全性。在实践中,主流浏览器使用Xorshift伪随机数生成器的变体,与伪随机数生成器(PRNG)一起使用时,会相当不错。

CSRNG和PRNG之间的区别在于PRNG使用单个种子,因此具有完全确定性,进而无法根据先前生成的数字预测CSRNG的输出。

2017年发布的Web密码学API或Crypto API定义了一个getRandomValues函数。根据caniuse的说法,有96.6%的用户使用的浏览器支持Crypto。我们发现,在我们的用户中,这个支持率接近99.9%,换句话说,Crypto API几乎可以在任何地方使用(甚至包括边缘设备,例如PS Vita)。这是一个重要的考虑因素:我们拥有15亿的独立用户,代表着超过一百万个不同的OS x 浏览器 x 浏览器版本 x 设备组合,因此我们必须确信所有用户都可以毫无问题地运行我们的代码。

使用Crypto API生成128位(16字节)随机数非常简单:

crypto.getRandomValues(new Uint8Array(16))

要将这些随机字节转换为RFC兼容的「版本4 UUID」,需要设置变体和版本位,然后将字节转换为以破折号分隔的十六进制数字。

另一种可能性是将File API与该URL.createObjectURL函数结合使用以获得包含UUID的Blob URL。对URL.createObjectURL的支持程度,与加密相似,为99.9%。

const url = URL.createObjectURL(new Blob())
url.substring(url.lastIndexOf('/')+ 1)

File API未指定应使用哪个版本的UUID,也未指明如何生成。实际上,基于Chromium的浏览器(Chrome和Edge)和WebKit重用其Crypto实现来生成随机字节,然后设置/清除位以创建「版本4 UUID」。当操作系统有相应支持时(CoCreateGuid在Windows,CFUUIDCreatemacOS上),Firefox会调用OS级别的函数,否则Firefox会选择使用类似Chromium和WebKit的Crypto。

最后,浏览器会基于操作系统来实现Crypto.getRandomValues,要么直接提供随机数,要么收集「无序状态」(entropy)然后定期将其转入到PRNG,从而使其具有加密安全性(CSPRNG)。

注意事项

我们的脚本已集成在数千个网站上,这些网站通常包括其他第三方脚本,并且每个脚本都可以重新定义/重载大多数JavaScript函数。我们发现有些脚本使Math.random函数重载,会始终返回相同的值,而另一些脚本则重新定义了window.URL属性以返回当前页面的URL。

有两种方法可以在不受第三方脚本影响的上下文中运行脚本:iframe和Web Workers。Web Workers更有趣,因为它们实例化更快,它们仅创建新的JavaScript执行上下文,而不是完整的DOM。

UUID生成实验

我们实现了一项功能,该功能可以生成带有Crypto的UUID(并依靠Math.random)并将其发送到我们的服务器,并设置A/B测试。这使我们能够检查大多数浏览器确实支持Crypto,并且我们的代码没有任何问题,而又不影响大多数用户。在可能的情况下,我们也对当前帧中运行的功能以及Web Worker中运行的功能进行了A/B测试。

对于已激活“uuid worker”功能的用户,我们测量出其中有50%的设备实例化一个worker需要花费200毫秒以上的时间。在我们的案例中,因为我们想在此过程中首先生成UUID,所以引入这样的延迟是不可接受的。然后,我们切换到基于File API的实现,使用Crypto作为后备,使用Math.random作为最后手段。

分析生成的UUID

我们最初发现,每千个请求中有将近2个请求带有重复的UUID。至少可以这么说,这令人警觉。

理论上讲,如果你连续85年每秒产生10亿个UUID,则发生一次碰撞(译注:即「哈希冲突」)的可能性为50%。以我们为例,我们每天将产生约10亿个UUID,因此我们应该可以安全使用约700万年。

上面两种表述之间的差异来自何处?

不同之处在于我们正在查看重复的请求,而不是冲突的标识符。重复的请求来自同一客户端,并被发送到服务器一次或多次,如下所示。这可能有多种原因,我们发现这些重复请求中的绝大多数都是由第三方脚本中的错误引起的。

另一方面,当一个以上的客户端使用给定的标识符时,就会发生冲突。在下面的架构中,客户端1和3之间都发生了冲突,客户端1和3都生成了以“ 0a87341d…” 开头的相同(红色)UUID 。请记住,从理论上讲,这是每天生成十亿个UUID 的“ 每700万年一次 ”事件。

冲突

在我们删除了重复的请求(来自相同的User-Agent,IP地址哈希,引荐来源网址等)之后,具有冲突UUID的请求数量大约等于1万个请求中的2个。但是,这还不是全部。当查看标识符的数量时,我们每百万获得约5个非唯一标识符

这是小了40倍,非常出乎意料:当你想到发生冲突时,你会想象两个非常不幸的用户生成了相同的标识符,但是在一天之内,全世界有成千上万个 不同的客户端 在生成相同的标识符。相同的UUID。请记住,浏览器提供的CSPRNG本质上与你可以在服务器上使用的CSPRNG一样好。这里发生了什么?

如果我们使用冲突的UUID接收所有请求,然后放大浏览器的User-Agent,那么我们将得到:

这些请求中几乎有三分之一是由Chrome Mobile 41.0生成的。令人惊讶的是,Chrome Mobile 41已有5年以上的历史了。这些请求的另一个共同点是基于IP发出的城市:将近三分之二来自Mountain View。Chrome Mobile 41.0发出的所有请求(100%)均来自山景城。你能想到一家总部设在那里的公司吗?

我们不是唯一得到类似观察结果的团体:在StackOverflow上的一个提问,是关于「在浏览器生成UUID」的,其中一个答案中提到了Googlebot是冲突的主要来源。在这个问题下,还提到Googlebot是因为其具有“伪” Math.random和“ new Date()”实现,或者「涉及重复的事件标识符」。虽然没有声明,但托管在山景城的Chrome Mobile 41实际上是Googlebot或其他Google服务。这将不再是个问题,因为Google在2019年12月宣布将开始更新Googlebot以在台式机和移动设备上使用最新版本的Chrome。

但这还不是全部。链接到在Mountain View中生成的标识符,请求会带有冲突的UUID达到了惊人的92%,而剩余8%请求的浏览器的User-Agent分布图,如下所示:

EvoPdf,WnvPdf和HiQPdf是.NET的HTML到PDF转换库,很可能它们在爬行带有我们脚本的页面时多次重复使用相同的标识符。PS Vita浏览器生成的UUID冲突似乎是合法的(与欺诈活动无关),并且可能是由于加密实现不佳所致:没有浏览器会生成与PS Vita生成的UUID冲突的UUID。他们的Crypto实现可能只是PRNG较弱。

最后,Internet Explorer的情况看起来不太像其加密实现较差,而更像是恶意脚本正在(滥)用它。UUID冲突的请求中有75%来自3个ISP:

  • Nobis Technology Group,
  • PSINet Inc.,
  • “m247 europe srl” (显然标签错误,应为“ PrivateInternetAccess ”).

从搜索引擎给出的结果中,我们发现,这些ISP提供VPN或公共代理。感觉有些不对劲,实际上这三个ISP仅占我们全球流量的0.1%,与我们在这里看到的75%相比,相去甚远。

从更深入的角度来看,在脚本加载3万次后,在32%的情况下,脚本由于网络错误而无法与广告服务器联系,并且在可以的情况下,该服务器阻止了98%以上的欺诈嫌疑请求(由DoubleVerify检查)。

结论

绝大多数的浏览器(99.9%)基于URL.createObjectURL或crypto.getRandomValues来提供生成随机(版本4)UUID所需的API。从主要浏览器的源代码中可以看到,这些功能的实现与服务器上的实现具有相似的质量。因此,非常令人惊讶的是,它们在每百万次请求中就会遇到5个非唯一标识符的大量冲突

仔细观察,这些API并不存在问题,而这些冲突似乎主要(92%)归因于Googlebot和其他一些与Google相关的服务。其余的冲突(8%)来自边缘浏览器(PS Vita),自动浏览器代理(HTML到PDF转换器)或与欺诈活动相关联,最有可能是由于中间人代理/代理。

对于我们的业务场景,每百万5个非唯一标识符的冲突率是可以接受的,特别是因为我们已经分析了其原因。为了避免在系统中出现这种“噪音”,我们正在设置一个过滤器,以维护一组重复的UUID,这些UUID被添加到阻止进入请求的阻止列表中。

致谢

感谢所有为本文及其所述内容做出贡献的人!首先,Nicolas Crovatti相信我们可以在浏览器中生成唯一的标识符,相信我可以深入浅出,并鼓励我写这篇文章;Thomas Azemard帮助我分析了数据(尤其是Chrome Mobile 41和PS Vita!);我的Format团队的同事们审阅了我的代码(特别感谢Benoit Ruiz审阅了它的无数次迭代!)和文章;我在SSP和Analytics(分析)团队中的同事们为他们在生产中实现这一目标提供了帮助(对于所有的不融洽,我们深表歉意!)(译注:原文sorry for all the non-hydrated macros!);最后是本杰明·戴维(Benjamin Davy),没有他,就不会有这篇文章。

原文地址:https://medium.com/teads-engineering/generating-uuids-at-scale-on-the-web-2877f529d2a2

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值