CSS 指纹识别:利用媒体查询与系统字体列表生成唯一用户标识
大家好,今天我们来聊聊一个略带争议但技术上非常有趣的话题:CSS 指纹识别。 这是一种利用CSS的特性来识别用户,即使他们清除了cookie或者使用了隐私模式。 我们将深入探讨这种技术的原理、实现方式,以及它所带来的伦理和社会问题。
什么是CSS指纹识别?
传统的用户追踪方式,例如 cookies 或 localStorage,容易被用户清除或禁用。 CSS 指纹识别则利用了浏览器渲染网页时的一些细微差别,这些差别来自于用户的操作系统、浏览器设置、以及安装的字体等。 通过收集这些信息,我们可以创建一个几乎唯一的“指纹”,用于识别用户。
CSS 指纹识别并非百分之百准确,但它可以与其他指纹识别技术结合使用,提高识别的准确率。
CSS指纹识别的原理
CSS指纹识别的核心在于,不同的浏览器和操作系统对CSS的解析和渲染可能存在细微的差异。这些差异可以体现在以下几个方面:
-
媒体查询 (Media Queries): 不同的设备和浏览器对媒体查询的支持程度和解析方式可能不同。例如,不同设备的分辨率、像素密度、设备方向等信息都可以通过媒体查询获取。
-
系统字体列表 (System Font List): 不同的操作系统和设备安装的字体不同。我们可以通过CSS来检测用户是否安装了特定的字体。
-
CSS 属性支持: 不同的浏览器对CSS属性的支持程度不同。例如,某些浏览器可能不支持某些CSS3属性。
-
渲染差异: 即使在相同的浏览器和操作系统下,由于硬件加速、图形驱动等因素,页面渲染也可能存在细微的差异。 这部分更复杂,涉及到 canvas 指纹识别等技术,我们今天重点关注前两点。
利用媒体查询生成指纹
媒体查询允许我们根据设备的特性应用不同的CSS样式。 通过精心设计的媒体查询,我们可以收集用户的设备信息,并将其用于生成指纹。
1. 获取屏幕分辨率:
我们可以使用 width 和 height 媒体特性来获取屏幕的分辨率。
@media (width: 320px) {
body::before {
content: "width:320px;";
}
}
@media (height: 480px) {
body::before {
content: "height:480px;";
}
}
这段CSS代码会检查屏幕的宽度和高度是否为320px和480px。 如果是,则会在 body 元素的 ::before 伪元素中插入相应的文本内容。
2. 获取设备像素密度:
使用 device-pixel-ratio 媒体特性可以获取设备的像素密度。
@media (device-pixel-ratio: 1) {
body::before {
content: "dpr:1;";
}
}
@media (device-pixel-ratio: 2) {
body::before {
content: "dpr:2;";
}
}
3. 获取设备方向:
使用 orientation 媒体特性可以获取设备的横竖屏方向。
@media (orientation: portrait) {
body::before {
content: "orientation:portrait;";
}
}
@media (orientation: landscape) {
body::before {
content: "orientation:landscape;";
}
}
JavaScript 代码收集信息:
我们需要使用 JavaScript 代码来读取 body::before 伪元素的内容,从而获取媒体查询的结果。
function getMediaQueryFingerprint() {
let fingerprint = "";
const beforeContent = window.getComputedStyle(document.body, "::before").getPropertyValue("content");
// Remove quotes around the content
fingerprint = beforeContent.replace(/["']/g, "");
return fingerprint;
}
const mediaQueryFingerprint = getMediaQueryFingerprint();
console.log("Media Query Fingerprint:", mediaQueryFingerprint);
完整示例:
<!DOCTYPE html>
<html>
<head>
<title>Media Query Fingerprint</title>
<style>
body::before {
content: "";
display: none; /* 隐藏伪元素 */
}
@media (width: 320px) {
body::before {
content: "width:320px;";
}
}
@media (width: 768px) {
body::before {
content: "width:768px;";
}
}
@media (height: 480px) {
body::before {
content: "height:480px;";
}
}
@media (height: 1024px) {
body::before {
content: "height:1024px;";
}
}
@media (device-pixel-ratio: 1) {
body::before {
content: "dpr:1;";
}
}
@media (device-pixel-ratio: 2) {
body::before {
content: "dpr:2;";
}
}
@media (orientation: portrait) {
body::before {
content: "orientation:portrait;";
}
}
@media (orientation: landscape) {
body::before {
content: "orientation:landscape;";
}
}
</style>
</head>
<body>
<h1>Media Query Fingerprint Example</h1>
<script>
function getMediaQueryFingerprint() {
let fingerprint = "";
const beforeContent = window.getComputedStyle(document.body, "::before").getPropertyValue("content");
// Remove quotes around the content
fingerprint = beforeContent.replace(/["']/g, "");
return fingerprint;
}
const mediaQueryFingerprint = getMediaQueryFingerprint();
console.log("Media Query Fingerprint:", mediaQueryFingerprint);
// You can send this fingerprint to your server for tracking
</script>
</body>
</html>
这个例子中,根据设备的宽度、高度、像素密度和方向,body::before 的内容会发生变化。 JavaScript 代码读取这个内容,并将其作为媒体查询指纹输出到控制台。 实际应用中,你需要将这个指纹发送到服务器端进行存储和分析。
媒体查询指纹的局限性:
- 通用性: 很多设备的分辨率和像素密度是相同的,导致指纹的唯一性降低。
- 用户修改: 用户可以修改浏览器的设置,例如改变分辨率,从而改变指纹。
利用系统字体列表生成指纹
不同的操作系统和设备安装的字体不同,我们可以利用这个特性来生成指纹。 原理是通过 CSS 字体栈来检测用户是否安装了特定的字体。
1. CSS 字体栈:
CSS 字体栈允许我们指定多个字体,浏览器会按照顺序尝试使用这些字体。 如果第一个字体不存在,则尝试使用第二个字体,以此类推。
body {
font-family: "MyCustomFont", sans-serif;
}
在这个例子中,如果用户安装了名为 "MyCustomFont" 的字体,浏览器会使用它。 否则,浏览器会使用默认的 sans-serif 字体。
2. 检测字体是否安装:
我们可以创建一个隐藏的 span 元素,并使用不同的字体栈来设置它的字体。 然后,我们可以测量 span 元素的宽度,如果宽度与默认字体的宽度不同,则说明用户安装了该字体。
<!DOCTYPE html>
<html>
<head>
<title>Font Fingerprint</title>
<style>
#font-test {
position: absolute;
left: -9999px;
top: -9999px;
visibility: hidden;
font-size: 16px; /* Important: Consistent font size */
width: auto;
height: auto;
white-space: nowrap; /* Prevent line breaks */
}
</style>
</head>
<body>
<h1>Font Fingerprint Example</h1>
<span id="font-test">abcdefghijklmnopqrstuvwxyz0123456789</span>
<script>
function detectFont(fontName) {
const element = document.getElementById("font-test");
const defaultWidth = element.offsetWidth;
element.style.fontFamily = `'${fontName}', sans-serif`; // Use single quotes inside backticks
const newWidth = element.offsetWidth;
return newWidth !== defaultWidth;
}
const fontsToTest = [
"Arial",
"Helvetica",
"Times New Roman",
"Courier New",
"Verdana",
"Georgia",
"Trebuchet MS",
"Impact",
"Comic Sans MS",
"Webdings",
"Wingdings"
// Add more fonts here
];
const installedFonts = {};
fontsToTest.forEach(font => {
installedFonts[font] = detectFont(font);
});
console.log("Installed Fonts:", installedFonts);
// You can send this information to your server for tracking
</script>
</body>
</html>
这个例子中,我们创建了一个隐藏的 span 元素,并使用 detectFont 函数来检测用户是否安装了指定的字体。 detectFont 函数首先测量 span 元素使用默认字体的宽度,然后设置字体栈为 '${fontName}', sans-serif,再次测量宽度。 如果宽度不同,则说明用户安装了该字体。
3. 生成字体指纹:
我们可以将检测到的字体列表转换为一个字符串,作为字体指纹。
function getFontFingerprint() {
let fingerprint = "";
for (const font in installedFonts) {
fingerprint += `${font}:${installedFonts[font] ? '1' : '0'};`;
}
return fingerprint;
}
const fontFingerprint = getFontFingerprint();
console.log("Font Fingerprint:", fontFingerprint);
在这个例子中,getFontFingerprint 函数将 installedFonts 对象转换为一个字符串,例如 "Arial:1;Helvetica:1;Times New Roman:1;…"。 这个字符串可以作为字体指纹发送到服务器端。
字体指纹的优势:
- 相对稳定: 用户安装的字体通常不会频繁更改,因此字体指纹相对稳定。
- 信息量大: 字体列表可以包含大量的信息,提高指纹的唯一性。
字体指纹的局限性:
- 字体列表有限: 可供检测的字体数量有限,不能覆盖所有可能的字体。
- 用户安装相同的字体: 不同的用户可能安装了相同的字体,导致指纹的唯一性降低。
结合媒体查询和字体列表生成指纹
为了提高指纹的唯一性,我们可以将媒体查询指纹和字体指纹结合起来使用。
1. 合并指纹:
将媒体查询指纹和字体指纹连接成一个字符串。
const combinedFingerprint = `${mediaQueryFingerprint}|${fontFingerprint}`;
console.log("Combined Fingerprint:", combinedFingerprint);
2. 哈希处理:
为了保护用户隐私,我们可以对指纹进行哈希处理。 哈希函数可以将任意长度的字符串转换为固定长度的哈希值。 常见的哈希算法包括 MD5、SHA-1、SHA-256 等。
async function hashFingerprint(fingerprint) {
const encoder = new TextEncoder();
const data = encoder.encode(fingerprint);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
hashFingerprint(combinedFingerprint).then(hashedFingerprint => {
console.log("Hashed Fingerprint:", hashedFingerprint);
// Send the hashed fingerprint to the server
});
这段代码使用 crypto.subtle.digest 函数计算 SHA-256 哈希值。 需要注意的是,crypto.subtle API 只能在安全上下文 (HTTPS) 中使用。
服务器端处理:
在服务器端,我们需要存储和分析指纹数据。 可以使用数据库来存储指纹,并使用算法来比较不同的指纹,从而识别用户。
指纹识别的流程:
- 客户端生成媒体查询指纹和字体指纹。
- 客户端将两个指纹合并,并进行哈希处理。
- 客户端将哈希后的指纹发送到服务器端。
- 服务器端在数据库中查找匹配的指纹。
- 如果找到匹配的指纹,则识别用户。 否则,将新的指纹添加到数据库中。
伦理和社会问题
CSS 指纹识别技术引发了一系列的伦理和社会问题。
- 隐私侵犯: 用户在不知情的情况下被追踪,侵犯了用户的隐私权。
- 透明度: 用户很难检测和阻止 CSS 指纹识别。
- 滥用: 该技术可能被用于恶意目的,例如定向广告、价格歧视等。
我们需要在使用 CSS 指纹识别技术时保持谨慎,并充分考虑其可能带来的负面影响。
防御 CSS 指纹识别
用户可以采取一些措施来防御 CSS 指纹识别。
- 禁用 JavaScript: 禁用 JavaScript 可以阻止脚本收集媒体查询和字体信息。 然而,这会影响网站的正常功能。
- 使用隐私浏览器: 一些隐私浏览器,例如 Brave 和 Tor,可以阻止指纹识别。
- 使用浏览器扩展: 有一些浏览器扩展可以随机化指纹信息,从而阻止指纹识别。 例如,Privacy Badger 和 NoScript。
- 定期清除浏览器数据: 清除浏览器缓存、cookies 和历史记录可以减少指纹的唯一性。
- 安装相同的字体: 安装常见的字体可以减少字体指纹的唯一性。
- 使用虚拟机: 在虚拟机中浏览网页可以隔离指纹信息。
以下是一个表格,总结了防御 CSS 指纹识别的常见方法:
| 防御方法 | 优点 | 缺点 |
|---|---|---|
| 禁用 JavaScript | 阻止脚本收集信息 | 影响网站的正常功能 |
| 使用隐私浏览器 | 自动阻止指纹识别 | 可能不兼容某些网站 |
| 使用浏览器扩展 | 可以随机化指纹信息 | 可能影响浏览器性能 |
| 定期清除浏览器数据 | 减少指纹的唯一性 | 每次清除后都需要重新登录网站 |
| 安装相同的字体 | 减少字体指纹的唯一性 | 需要手动安装字体 |
| 使用虚拟机 | 隔离指纹信息 | 性能开销大 |
总结
我们今天探讨了 CSS 指纹识别的原理、实现方式,以及它所带来的伦理和社会问题。 虽然这种技术可以用于识别用户,但也存在着隐私侵犯的风险。 用户可以采取一些措施来防御 CSS 指纹识别,例如禁用 JavaScript、使用隐私浏览器、使用浏览器扩展等。 我们需要在使用 CSS 指纹识别技术时保持谨慎,并充分考虑其可能带来的负面影响。
媒体查询可以获取设备分辨率和像素密度,系统字体列表可以检测用户安装的字体。 结合使用这两种技术可以生成更精确的用户指纹,但需要谨慎使用以避免侵犯用户隐私。 保护用户隐私是开发者的责任,我们需要在技术发展的同时,也要关注伦理和社会问题。
更多IT精英技术系列讲座,到智猿学院