-
Notifications
You must be signed in to change notification settings - Fork 683
/
Copy path生成浏览器唯一稳定 ID 的探索.md.html
641 lines (544 loc) · 44.5 KB
/
生成浏览器唯一稳定 ID 的探索.md.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>生成浏览器唯一稳定 ID 的探索.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/文章/AQS 万字图文全面解析.md.html">AQS 万字图文全面解析.md.html</a>
</li>
<li>
<a href="/文章/Docker 镜像构建原理及源码分析.md.html">Docker 镜像构建原理及源码分析.md.html</a>
</li>
<li>
<a href="/文章/ElasticSearch 小白从入门到精通.md.html">ElasticSearch 小白从入门到精通.md.html</a>
</li>
<li>
<a href="/文章/JVM CPU Profiler技术原理及源码深度解析.md.html">JVM CPU Profiler技术原理及源码深度解析.md.html</a>
</li>
<li>
<a href="/文章/JVM 垃圾收集器.md.html">JVM 垃圾收集器.md.html</a>
</li>
<li>
<a href="/文章/JVM 面试的 30 个知识点.md.html">JVM 面试的 30 个知识点.md.html</a>
</li>
<li>
<a href="/文章/Java IO 体系、线程模型大总结.md.html">Java IO 体系、线程模型大总结.md.html</a>
</li>
<li>
<a href="/文章/Java NIO浅析.md.html">Java NIO浅析.md.html</a>
</li>
<li>
<a href="/文章/Java 面试题集锦(网络篇).md.html">Java 面试题集锦(网络篇).md.html</a>
</li>
<li>
<a href="/文章/Java-直接内存 DirectMemory 详解.md.html">Java-直接内存 DirectMemory 详解.md.html</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决(上).md.html">Java中9种常见的CMS GC问题分析与解决(上).md.html</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决(下).md.html">Java中9种常见的CMS GC问题分析与解决(下).md.html</a>
</li>
<li>
<a href="/文章/Java中的SPI.md.html">Java中的SPI.md.html</a>
</li>
<li>
<a href="/文章/Java中的ThreadLocal.md.html">Java中的ThreadLocal.md.html</a>
</li>
<li>
<a href="/文章/Java线程池实现原理及其在美团业务中的实践.md.html">Java线程池实现原理及其在美团业务中的实践.md.html</a>
</li>
<li>
<a href="/文章/Java魔法类:Unsafe应用解析.md.html">Java魔法类:Unsafe应用解析.md.html</a>
</li>
<li>
<a href="/文章/Kafka 源码阅读笔记.md.html">Kafka 源码阅读笔记.md.html</a>
</li>
<li>
<a href="/文章/Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html">Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB Buffer Pool.md.html">MySQL · 引擎特性 · InnoDB Buffer Pool.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB IO子系统.md.html">MySQL · 引擎特性 · InnoDB IO子系统.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 事务系统.md.html">MySQL · 引擎特性 · InnoDB 事务系统.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 同步机制.md.html">MySQL · 引擎特性 · InnoDB 同步机制.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 数据页解析.md.html">MySQL · 引擎特性 · InnoDB 数据页解析.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB崩溃恢复.md.html">MySQL · 引擎特性 · InnoDB崩溃恢复.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · 临时表那些事儿.md.html">MySQL · 引擎特性 · 临时表那些事儿.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 半同步复制.md.html">MySQL 主从复制 半同步复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 基于GTID复制.md.html">MySQL 主从复制 基于GTID复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制.md.html">MySQL 主从复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 事务日志(redo log和undo log).md.html">MySQL 事务日志(redo log和undo log).md.html</a>
</li>
<li>
<a href="/文章/MySQL 亿级别数据迁移实战代码分享.md.html">MySQL 亿级别数据迁移实战代码分享.md.html</a>
</li>
<li>
<a href="/文章/MySQL 从一条数据说起-InnoDB行存储数据结构.md.html">MySQL 从一条数据说起-InnoDB行存储数据结构.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:事务和锁的面纱.md.html">MySQL 地基基础:事务和锁的面纱.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据字典.md.html">MySQL 地基基础:数据字典.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据库字符集.md.html">MySQL 地基基础:数据库字符集.md.html</a>
</li>
<li>
<a href="/文章/MySQL 性能优化:碎片整理.md.html">MySQL 性能优化:碎片整理.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html">MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:如何在日志中轻松定位大事务.md.html">MySQL 故障诊断:如何在日志中轻松定位大事务.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:教你快速定位加锁的 SQL.md.html">MySQL 故障诊断:教你快速定位加锁的 SQL.md.html</a>
</li>
<li>
<a href="/文章/MySQL 日志详解.md.html">MySQL 日志详解.md.html</a>
</li>
<li>
<a href="/文章/MySQL 的半同步是什么?.md.html">MySQL 的半同步是什么?.md.html</a>
</li>
<li>
<a href="/文章/MySQL中的事务和MVCC.md.html">MySQL中的事务和MVCC.md.html</a>
</li>
<li>
<a href="/文章/MySQL事务_事务隔离级别详解.md.html">MySQL事务_事务隔离级别详解.md.html</a>
</li>
<li>
<a href="/文章/MySQL优化:优化 select count().md.html">MySQL优化:优化 select count().md.html</a>
</li>
<li>
<a href="/文章/MySQL共享锁、排他锁、悲观锁、乐观锁.md.html">MySQL共享锁、排他锁、悲观锁、乐观锁.md.html</a>
</li>
<li>
<a href="/文章/MySQL的MVCC(多版本并发控制).md.html">MySQL的MVCC(多版本并发控制).md.html</a>
</li>
<li>
<a href="/文章/QingStor 对象存储架构设计及最佳实践.md.html">QingStor 对象存储架构设计及最佳实践.md.html</a>
</li>
<li>
<a href="/文章/RocketMQ 面试题集锦.md.html">RocketMQ 面试题集锦.md.html</a>
</li>
<li>
<a href="/文章/SnowFlake 雪花算法生成分布式 ID.md.html">SnowFlake 雪花算法生成分布式 ID.md.html</a>
</li>
<li>
<a href="/文章/Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html">Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html</a>
</li>
<li>
<a href="/文章/Spring Boot 教程:如何开发一个 starter.md.html">Spring Boot 教程:如何开发一个 starter.md.html</a>
</li>
<li>
<a href="/文章/Spring MVC 原理.md.html">Spring MVC 原理.md.html</a>
</li>
<li>
<a href="/文章/Spring MyBatis和Spring整合的奥秘.md.html">Spring MyBatis和Spring整合的奥秘.md.html</a>
</li>
<li>
<a href="/文章/Spring 帮助你更好的理解Spring循环依赖.md.html">Spring 帮助你更好的理解Spring循环依赖.md.html</a>
</li>
<li>
<a href="/文章/Spring 循环依赖及解决方式.md.html">Spring 循环依赖及解决方式.md.html</a>
</li>
<li>
<a href="/文章/Spring中眼花缭乱的BeanDefinition.md.html">Spring中眼花缭乱的BeanDefinition.md.html</a>
</li>
<li>
<a href="/文章/Vert.x 基础入门.md.html">Vert.x 基础入门.md.html</a>
</li>
<li>
<a href="/文章/eBay 的 Elasticsearch 性能调优实践.md.html">eBay 的 Elasticsearch 性能调优实践.md.html</a>
</li>
<li>
<a href="/文章/不可不说的Java“锁”事.md.html">不可不说的Java“锁”事.md.html</a>
</li>
<li>
<a href="/文章/互联网并发限流实战.md.html">互联网并发限流实战.md.html</a>
</li>
<li>
<a href="/文章/从ReentrantLock的实现看AQS的原理及应用.md.html">从ReentrantLock的实现看AQS的原理及应用.md.html</a>
</li>
<li>
<a href="/文章/从SpringCloud开始,聊微服务架构.md.html">从SpringCloud开始,聊微服务架构.md.html</a>
</li>
<li>
<a href="/文章/全面了解 JDK 线程池实现原理.md.html">全面了解 JDK 线程池实现原理.md.html</a>
</li>
<li>
<a href="/文章/分布式一致性理论与算法.md.html">分布式一致性理论与算法.md.html</a>
</li>
<li>
<a href="/文章/分布式一致性算法 Raft.md.html">分布式一致性算法 Raft.md.html</a>
</li>
<li>
<a href="/文章/分布式唯一 ID 解析.md.html">分布式唯一 ID 解析.md.html</a>
</li>
<li>
<a href="/文章/分布式链路追踪:集群管理设计.md.html">分布式链路追踪:集群管理设计.md.html</a>
</li>
<li>
<a href="/文章/动态代理种类及原理,你知道多少?.md.html">动态代理种类及原理,你知道多少?.md.html</a>
</li>
<li>
<a href="/文章/响应式架构与 RxJava 在有赞零售的实践.md.html">响应式架构与 RxJava 在有赞零售的实践.md.html</a>
</li>
<li>
<a href="/文章/大数据算法——布隆过滤器.md.html">大数据算法——布隆过滤器.md.html</a>
</li>
<li>
<a href="/文章/如何优雅地记录操作日志?.md.html">如何优雅地记录操作日志?.md.html</a>
</li>
<li>
<a href="/文章/如何设计一个亿级消息量的 IM 系统.md.html">如何设计一个亿级消息量的 IM 系统.md.html</a>
</li>
<li>
<a href="/文章/异步网络模型.md.html">异步网络模型.md.html</a>
</li>
<li>
<a href="/文章/当我们在讨论CQRS时,我们在讨论些神马?.md.html">当我们在讨论CQRS时,我们在讨论些神马?.md.html</a>
</li>
<li>
<a href="/文章/彻底理解 MySQL 的索引机制.md.html">彻底理解 MySQL 的索引机制.md.html</a>
</li>
<li>
<a href="/文章/最全的 116 道 Redis 面试题解答.md.html">最全的 116 道 Redis 面试题解答.md.html</a>
</li>
<li>
<a href="/文章/有赞权限系统(SAM).md.html">有赞权限系统(SAM).md.html</a>
</li>
<li>
<a href="/文章/有赞零售中台建设方法的探索与实践.md.html">有赞零售中台建设方法的探索与实践.md.html</a>
</li>
<li>
<a href="/文章/服务注册与发现原理剖析(Eureka、Zookeeper、Nacos).md.html">服务注册与发现原理剖析(Eureka、Zookeeper、Nacos).md.html</a>
</li>
<li>
<a href="/文章/深入浅出Cache.md.html">深入浅出Cache.md.html</a>
</li>
<li>
<a href="/文章/深入理解 MySQL 底层实现.md.html">深入理解 MySQL 底层实现.md.html</a>
</li>
<li>
<a href="/文章/漫画讲解 git rebase VS git merge.md.html">漫画讲解 git rebase VS git merge.md.html</a>
</li>
<li>
<a class="current-tab" href="/文章/生成浏览器唯一稳定 ID 的探索.md.html">生成浏览器唯一稳定 ID 的探索.md.html</a>
</li>
<li>
<a href="/文章/缓存 如何保证缓存与数据库的双写一致性?.md.html">缓存 如何保证缓存与数据库的双写一致性?.md.html</a>
</li>
<li>
<a href="/文章/网易严选怎么做全链路监控的?.md.html">网易严选怎么做全链路监控的?.md.html</a>
</li>
<li>
<a href="/文章/美团万亿级 KV 存储架构与实践.md.html">美团万亿级 KV 存储架构与实践.md.html</a>
</li>
<li>
<a href="/文章/美团点评Kubernetes集群管理实践.md.html">美团点评Kubernetes集群管理实践.md.html</a>
</li>
<li>
<a href="/文章/美团百亿规模API网关服务Shepherd的设计与实现.md.html">美团百亿规模API网关服务Shepherd的设计与实现.md.html</a>
</li>
<li>
<a href="/文章/解读《阿里巴巴 Java 开发手册》背后的思考.md.html">解读《阿里巴巴 Java 开发手册》背后的思考.md.html</a>
</li>
<li>
<a href="/文章/认识 MySQL 和 Redis 的数据一致性问题.md.html">认识 MySQL 和 Redis 的数据一致性问题.md.html</a>
</li>
<li>
<a href="/文章/进阶:Dockerfile 高阶使用指南及镜像优化.md.html">进阶:Dockerfile 高阶使用指南及镜像优化.md.html</a>
</li>
<li>
<a href="/文章/铁总在用的高性能分布式缓存计算框架 Geode.md.html">铁总在用的高性能分布式缓存计算框架 Geode.md.html</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析(上).md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析(上).md.html</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析(下).md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析(下).md.html</a>
</li>
<li>
<a href="/文章/面试最常被问的 Java 后端题.md.html">面试最常被问的 Java 后端题.md.html</a>
</li>
<li>
<a href="/文章/领域驱动设计在互联网业务开发中的实践.md.html">领域驱动设计在互联网业务开发中的实践.md.html</a>
</li>
<li>
<a href="/文章/领域驱动设计的菱形对称架构.md.html">领域驱动设计的菱形对称架构.md.html</a>
</li>
<li>
<a href="/文章/高效构建 Docker 镜像的最佳实践.md.html">高效构建 Docker 镜像的最佳实践.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>生成浏览器唯一稳定 ID 的探索</h1>
<h3><strong>1. 背景</strong></h3>
<p>项目的 PC Web 端在不同浏览器有不同的登录态,同一浏览器多个窗口多个 tab 共用一个登录态(隐身无痕模式除外)。为了能够实现 PC 浏览器端设备管理功能,让用户能看到自己在浏览器的多个登录态,并能选择踢掉其中的部分或全部,就需要一种能够区分不同浏览器的方法,这个在移动端 App 就是设备 ID, 我们可以迁移一下,在 PC 端,一个浏览器就好比移动端的一个手机设备,只要能够生成浏览器 ID,就能够区分不同的登录态。</p>
<p><img src="assets/v2-e1f82d5e3efaa2a48f46def1c7cdb636_1440w.jpg" alt="img" /></p>
<p>这里说的浏览器 ID 在业界早有研究,叫<strong>浏览器指纹</strong>。浏览器指纹,是 EFF(电子前哨基金会)提出的一项追踪技术,通过浏览器对网站可见的配置来匿名识别浏览器,在 50 万份不同浏览器数据分析中,在某些场景下,94%的浏览器具有唯一的指纹。当时,EFF 通过提取浏览器的 8 个特征值,{ <code>user agent</code>, <code>plugins</code>, <code>fonts</code>, <code>video</code>, <code>supercookies</code>, <code>http accept</code>, <code>timezone</code>, <code>cookie enabled</code> }, 综合起来哈希生成一个指纹值。每项浏览器特征都包含不同 bit 的信息熵,一个 bit 有两种可能性,2 bits 有四种,3 bits 就有八种,提取的八项特征共包含 18.1 bits 的信息,这意味着在 286,777 个指纹中才会出现一个与你重复的浏览器指纹[1]。</p>
<p>浏览器指纹是<strong>无状态</strong>的,它无需用户授权便可收集信息,并且不会像 Cookie 一样会被用户禁止或清除,只要你的指纹是唯一的,你就能够被唯一识别和跟踪。当然,一个事物的好坏是取决于怎么使用它,浏览器指纹也可用于一些好的方面,比如:反欺诈,防止刷票、黄牛、机器人,异地或者可疑登录提示,多设备管理,或者在注重用户隐私的情况下,进行一些数据分析,提高用户体验。</p>
<p><img src="assets/v2-ba92954553af3997ef164ebbdc186ae5_1440w.jpg" alt="img" /></p>
<h3><strong>2. 分析</strong></h3>
<p>抛开业界已经存在的一些方案,我们可以先开点脑洞去分析。要实现的目标是给一个设备的一个浏览器生成一个<strong>唯一</strong>和<strong>稳定</strong>的 ID, 这里无非就是从硬件、操作系统、浏览器的维度去分析,看看有什么办法可以区分。</p>
<h3><strong>2.1. 一些参考维度</strong></h3>
<h3><strong>2.1.1. MAC 地址</strong></h3>
<p>MAC 地址又称为物理地址、硬件地址,它是一个用来确认网络设备位置的位址,在 OSI 七层模型中,第二层数据链路层负责 MAC 地址,网卡的 MAC 地址通常是由网卡厂家烧入网卡里的,能在网络中唯一标识一个网卡。</p>
<p>如果我们能获得 MAC 地址,我们就能唯一标识一台电脑(一个网卡),这能在很大程度保证生成 ID 的唯一性和稳定性。但是想在浏览器端获取 MAC 地址基本是做不到的,如果是 Windows 用户,<strong>一种可能的方法</strong>是让用户安装 ActiveX 控件。从服务端也很难操作,获取到的 MAC 地址可能是中间路由器的 MAC 地址,而不是浏览器设备的 MAC 地址。另外,MAC 地址也是可以伪造的。</p>
<h3><strong>2.1.2. IP 地址</strong></h3>
<p>当设备连接网络,设备将被分配一个 IP 地址,用作标识。IP 地址记录在 OSI 模型中的第三层网络层。服务端从 HTTP 请求中可以分析得到 IP 地址,浏览器端可以通过 <code>WebRTC</code> 网页实时通信技术 的 <code>RTCPeerConnection</code> 的 API, 获取到客户端的 IP 地址。IP 地址是可能重复的,可能有多个设备处在一个局域网下,共享一个公网 IP。 IP 地址也是多变的,计算机经常会在不同的网络和代理下使用。但是,IP 地址的唯一性比较强,两个设备的碰撞率是很低的,如果能利用 IP 地址去增强浏览器 ID 的唯一性是非常有价值的。</p>
<h3><strong>2.1.3. Cookie</strong></h3>
<p><code>Cookie</code> 是存在浏览器上的一段信息,并能在服务端和浏览器之间传递,一般用于维持登录态或者保存一些信息。Cookie 中保存的信息是可以标识用户的,甚至可以追踪用户的行为和隐私数据,进而为产品决策提供数据。虽然 Cookie 本身或者其他诸如 localStorage 等存储技术不能提供浏览器特征参数(判断是否禁用 Cookie 可以作为参数),但是可以作为辅助手段存储浏览器 ID,当用户登录态过期或者伪造浏览器特征时,我们可以从 Cookie 和其他存储中取到之前的 ID 继续使用。</p>
<h3><strong>2.1.4. User Agent</strong></h3>
<p><code>User Agent</code>, 用户代理,简称 UA,它是一个特殊字符串头,使得服务器能够识别客户使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等。一般稍微熟悉一点 Web 开发的都会自然想到 UA 能够作为特征生成浏览器 ID, 它大概包含 5.34 bits 的信息熵,具有一定的唯一性,但是稳定性较差,比如:用户可能很容易升级浏览器版本,这样 UA 就会发生变化,可行的解决思路是:只取浏览器型号,不取浏览器版本,这样比较稳定;因为 UA 比较容易伪造,也可通过其他维度去代替,当这些维度发生变化,可以认为 UA 发生了改变。</p>
<p>可以看到,通过单一维度去生成 ID 是不太可靠的,唯一性和稳定性都无法保证,更有效的方式是通过多个维度组合起来形成一个综合指纹。通过调研,目前业界比较领先的开源库是 <code>FingerprintJS</code>, 现在已经更新到 v3.2.0 版本,也一直处于迭代状态。</p>
<h3><strong>2.2. FingerprintJS</strong></h3>
<p><strong>FingerprintJS</strong>(下文简称 FPJS)是一个浏览器指纹库,同时具有开源版和 Pro 版(付费版),可查询浏览器属性并从中计算出哈希值。与 Cookie 和本地存储不同,指纹在隐身模式下保持不变,甚至在浏览器数据清除时也是如此。</p>
<p>它最早的灵感就是来源于 EFF 提出浏览器指纹的概念,在此基础上,又增加了许多特征参数,包括一些新型的识别技术,比如:Canvas、AudioContext 等,在不断地迭代中,优化这些参数,并用最快的方式生成指纹。</p>
<h3><strong>2.2.1. 开源版本</strong></h3>
<p><img src="assets/v2-7ba81f477d9a1aeca1c53ccfa18c11fc_1440w.jpg" alt="img" /></p>
<p>开源版本通过浏览器捕捉到各种指标并通过哈希算法组合成一个指纹,我把这些指标分成四类,浏览器特性、存储、媒体查询、其它。具体如上图所示。目前 GitHub Star 数有 <strong>14.1k</strong>, 并被 <strong>8000+</strong> 网站使用。</p>
<h3><strong>2.2.2. 唯一性和稳定性</strong></h3>
<p>到底什么样的指纹是唯一和稳定的?</p>
<p><strong>唯一性</strong>指在不同的设备,或者不同的浏览器上,每个指纹都是不同的。实际情况是,总会有各种指标都完全相同的用户,那么就会生成一样的指纹,比如唯一性能达到 95%,那 100 个指纹中,有 5 个是重复的。</p>
<p><strong>稳定性</strong>指用户每次打开相同设备的相同浏览器,生成的指纹是一样的。如果用户没有修改设备或者浏览器的设置,且浏览器没有升级,生成的 ID 是不会变的。但是在实际情况中,如果选用了一些不稳定的指标参与计算或者用户使用了一些反指纹手段,那么可能每次生成的都会不一样。</p>
<p>为了提高唯一和稳定,我们的指纹不仅应该结合许多指标,还应该尽量筛选出区分度大和稳定性高的指标,需要找到平衡唯一和稳定的指标组合。在控制变量的情况下,随着指标的增多,唯一性增强,但同时稳定性会减弱。</p>
<p><img src="assets/v2-62d52f9817bb6d14e64625db76fc5a5b_1440w.jpg" alt="img" /></p>
<p>在四象限中,我们优先抛弃那些唯一性和稳定性低的指标,比如图中的电池电量。其它的指标中,可以通过用各种组合进行数据采集和验证,迭代出最平衡的组合。</p>
<h3><strong>2.2.3. 一些有趣的指标</strong></h3>
<p><strong>Canvas 指纹</strong></p>
<p><code>Canvas</code>(画布)是 HTML5 中的一种动态绘图标签,可以用它来绘制图片。在不同操作系统、不同浏览器上,Canvas 绘制的图像将以不同的方式呈现,具有很强的唯一性。原理是:在图片格式上,浏览器使用不同的图形处理引擎、图像导出选项、压缩级别,在系统层面,操作系统有不同的字体,它们使用不同的算法和设置来进行抗锯齿和子像素渲染。另外,Canvas 具有良好的兼容性,几乎被所有主流浏览器支持。</p>
<p>在具体代码上,通过 Canvas 绘图 API 绘制文字或图形后,通过 <code>canvas.toDataURL()</code> 方法获得 base64 编码,根据需要可再 hash 成指纹。</p>
<p><strong>判断是否包含某字体</strong></p>
<p>首先,前端不存在兼容性比较好的原生方法判断是否包含某字体,那么该怎么判断呢?</p>
<p>不同字体显示相同的文案时,宽度是不同的。我们可以利用这一点,设置三种默认字体,'monospace', 'sans-serif', 'serif', 新建一个 span 标签,设置 font-family 为当前字体和默认字体, 设置另外一个 span 为默认字体,如果存在当前字体,两个 span 的宽度或者高度是不一样的,如果完全一致,则代表不存在该字体,第一个 span 回退回默认字体。</p>
<p><strong>Math 类几个函数精度不同</strong></p>
<p>不同的操作系统和架构,不同的浏览器的几个 Math 数学函数可能产生不同结果。比如 Math.sin(-1e300),在不同的系统和浏览器上算出来的值可能是不同的。有趣的是,在代码注释中可以看到<strong>相关链接</strong>,是来自于 tor 浏览器,这是一个宣称保护隐私的浏览器,具备一定的反指纹策略。所以,有时候有效信息可以从对手那里获取。</p>
<p><strong>音频指纹</strong></p>
<p><strong>查看链接</strong>,原理和 Canvas 类似,都是利用硬件和软件的差异,一个生成音频,一个生成图片。</p>
<h3><strong>2.2.4. Pro 付费版本</strong></h3>
<p><img src="assets/v2-b6cfc81c0d888c8d0fc00306df717999_1440w.jpg" alt="img" /></p>
<p>开源版本和<strong>专业版</strong>主要区别在于,开源版本仅仅运行于浏览器端,浏览器通过 JS 获取到一系列指标后计算哈希值。当两个用户有同样的设备和浏览器时,他们可能生成同样的指纹,从而无法区分两个用户(从提取的指标来看,它们的确是相同的,只是有些业务场景需要更加细分的手段去区分用户)。专业版建立在开源版的基础上,并增加了很多服务器端的手段去辅助识别用户,比如:IP 地址、Cookie、本地存储、存储历史记录进行模糊匹配和处理浏览器升级、服务器端分析和机器学习,并最终由服务端生成指纹。</p>
<p><img src="assets/v2-b2e2f7bc9b1c6654a30ad8869a360eab_1440w.jpg" alt="img" /></p>
<p>图 6. 官方提供的一些对比</p>
<p>可以看出,专业版增加了一些服务端检测技术后,在生成 ID 的唯一性和稳定性都有很大的提高,另外,ID 在服务端生成也更安全可靠。笔者曾试图调试研究专业版压缩混淆的代码,但能力和时间有限,只能看得出小部分。后来,尝试通过实验和对比的方法,了解专业版是怎么提高唯一性和稳定性的。</p>
<h3><strong>分析和发现</strong></h3>
<p><img src="assets/v2-504158c0cbe7ee8bcd4fb3bc6f1d62b3_1440w.jpg" alt="img" /></p>
<p><img src="assets/v2-2f950787bcc29fe62176b84407fa42fb_1440w.jpg" alt="img" /></p>
<p>尝试 1:IP 是否会影响指纹</p>
<p>结果:通过 VPN 或者连接不同的网络,指纹的生成是稳定的,<strong>直接切换 IP 并不影响指纹</strong>。可以理解,因为 IP 是非常不稳定的,用户很容易切换 IP,导致指纹变化。个人看法,IP 只是作为辅助手段进行校验,比如曾经有使用某个 IP 的历史记录,就可以佐证是之前生成的某个指纹,即使某些指标发生异动。</p>
<p>尝试 2:伪造某些指标是否会影响指纹</p>
<p>开源版:只要修改了提取的某个参数值,生成的指纹就会改变。</p>
<p>专业版:在 Chrome 的无痕模式下,通过插件(Fingerprint Spoofing)或者断点通过 <code>Object.defineProperty</code> 等方式修改某些指标的值是会影响指纹的。但在普通模式下,修改参数不会影响指纹。为什么呢,我们会很容易去想普通模式和隐身模式有什么不同。经过一些尝试后,笔者把目标锁定在 Cookie 、本地存储和缓存上,专业版生成指纹后,存储在 Cookie 和本地存储中,下一次生成指纹时,先判断是否已存在 Cookie,有且符合历史记录则直接取,没有再重新生成。</p>
<p><img src="assets/v2-4f895d3ad2261567de9f960c35770641_1440w.jpg" alt="img" /></p>
<p><img src="assets/v2-ec9272e083423defaa7f09924c823c68_1440w.jpg" alt="img" /></p>
<p>尝试 3:伪造某些指标并清除 Cookie 、本地存储和缓存后是否影响专业版指纹</p>
<p>结果:会影响,基本上可以实现随机指纹。所以,基本可以确认,<strong>当 Cookie 等内容存在时,能够匹配历史记录里的值就直接从里面取值,这样能在很大程度上保持指纹的稳定。在实验中,Cookie、本地存储和缓存都可以用于保持稳定。</strong></p>
<p>其他发现:</p>
<ul>
<li>当伪造的值不具有迷惑性时,可能绕不过服务端的分析。比如:我在 Win10 Chrome 上将 <code>navigator.platform</code> 修改为 MacIntel, 也就是 Mac 的标识时,指纹是稳定的。猜想服务端应该分析到这个指标与整个模型有偏差,所以矫正了该指标,依然使用了之前的指纹。</li>
</ul>
<p><img src="assets/v2-bee45fb15d2fed145b42bd141c112785_1440w.jpg" alt="img" /></p>
<p><img src="assets/v2-daaf2a6227432967a67c14d5e23ecb63_1440w.jpg" alt="img" /></p>
<p><img src="assets/v2-a08c5df832c3067229e5c82f6eb44065_1440w.jpg" alt="img" /></p>
<p><img src="assets/v2-ecca30dfb036d4d2ad81e6c4a7b2b3c9_1440w.jpg" alt="img" /></p>
<ul>
<li>另外发现 Pro 中引入了很多第三方分析,比如 GA 谷歌分析、linkedin 领英分析、Twitter 分析、FB 脸书分析,这些大公司为了推广广告营销等业务,可能在浏览器指纹上的造诣是很高的,Pro 版本可能利用了这些第三方分析的结果辅助指纹的唯一性和稳定性,当然,这些第三方分析同样也是利用 Cookie 等方式标记浏览器和用户,利用清除的方法可以规避追踪。</li>
</ul>
<h3><strong>3. 采用的做法</strong></h3>
<ul>
<li>首先将 FPJS v3.0.6 在项目中打点测试,在几百万~几千万的独立用户数量级上,用独立用户数 / 独立指纹数 约等于 1.3,这个在移动端 App 的数据大约是 1.1 ~ 1.2,一个用户可能有多个账号,可能用多个账号登录同一个浏览器,也可能同一个账号登录多个浏览器,是多对多的关系。在几百万到几千万的变化上,这个比例相对比较稳定,至少随着用户数的增加,稳定性不会降低;另外,这个比例跟移动端比没有偏差太多,还算合格,后面可以结合其他手段提高唯一性;其实在实现像设备管理这样的功能时,只需要保证同一用户在不同浏览器上生成不同的指纹即可。<strong>综上所述,可以采用 FPJS 作为前端生成指纹的选择,并结合 Cookie 等策略优化唯一和稳定。</strong></li>
</ul>
<p><img src="assets/v2-61d5a8ea0c9d19ca6e190c20e8019ee3_1440w.jpg" alt="img" /></p>
<p>图 12. 指纹生成具体做法</p>
<ul>
<li>具体做法:登录时,前端生成指纹通过 HTTP 请求传给后端,后端经过一些处理后,再 hash 以后生成一个值,通过 set cookie 返回前端,当前端传空值时,后端生成一个值返回前端。当用户再进行登录时,如果已存在 Cookie,则取 Cookie 的值,如果不存在,则根据前面逻辑生成新的。后续如用到指纹时,通过 HTTP 请求带上 Cookie 即可。</li>
<li>后续如何优化:</li>
</ul>
<ol>
<li>FPJS 目前已升级到 v3.2.0 版本,增加了一些指标维度,并优化了一些指标的具体使用,可以打点测试后,看看唯一性是否提高,如果可行,可以考虑新的指纹生成升级到新版本。</li>
<li>目前没有历史记录,可以考虑记录历史指纹,帮助验证 Cookie 的有效性及辅助判断当前指纹是否异常等。</li>
<li>通过历史 IP 辅助验证 Cookie 的有效性和唯一性。</li>
</ol>
<h3><strong>4. 反浏览器指纹</strong></h3>
<ul>
<li>禁用 Cookie 或 JS</li>
</ul>
<p>比较严格,应该可以防止大部分的浏览器指纹。但是,很多网站都需要依托 Cookie 维持登录态 和 JS 实现逻辑,禁用后可能没办法使用网站或者影响体验。</p>
<ul>
<li>隐身模式加随机身份</li>
</ul>
<p>这个组合可以保证大部分场景下生成随机的指纹,隐身模式下退出后会清理 Cookie、本地存储和缓存,随机的身份(指标组合)可以保证生成不了历史的指纹,从而不被跟踪。</p>
<ul>
<li>tor 浏览器</li>
</ul>
<p>tor 浏览器号称是最安全的浏览器,它在反指纹上做了很多工作,它的主要策略是<strong>让所有 tor 浏览器的用户都拥有完全相同的指纹</strong>,无论你是什么设备或者操作系统,尽量降低指纹的唯一性。另外,tor 天然就是隐身模式,可以更换身份以及可以隐藏真实 IP。当然,在使用体验上,tor 相对会慢一点,毕竟经历了这么多安全策略。</p>
<ul>
<li>攻与防</li>
</ul>
<p>浏览器指纹技术根植于 Web 诞生以来就存在的机制里,在可预见的未来,要完全摆脱它是很困难的。随着浏览器指纹攻防两端技术的不断提升,这种竞争可能愈演愈烈。试想,<strong>随着技术的不断提升,希望保护隐私的用户为了不被追踪就需要更高的防御手段,这势必会影响用户体验,造成两败俱伤的局面。</strong> 其实,就像上文说的,浏览器指纹只是一种技术,它的好与坏取决于你所身处的位置以及怎么使用它,<strong>笔者觉得,可以建立一种机制,允许它的合理存在,并合理地使用它,用于一些合法合规,并且对用户友好的场景。</strong></p>
<h3><strong>5. 总结</strong></h3>
<p>本文从几个参考维度开始分析,介绍了浏览器指纹的生成原理。而后,展开分析了 FingerprintJS 的开源版和 Pro 版,最后在项目中进行了首次应用,在后续迭代的过程中,会不断根据实际情况进行优化。本文只是初步探索与尝试,如有任何错误和不足,欢迎批评指正。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/文章/漫画讲解 git rebase VS git merge.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/文章/缓存 如何保证缓存与数据库的双写一致性?.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"7099806f5e628b66","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>