CSS 指纹识别:利用媒体查询与系统字体列表生成的唯一用户标识

CSS 指纹识别:利用媒体查询与系统字体列表生成唯一用户标识

大家好,今天我们来聊聊一个略带争议但技术上非常有趣的话题:CSS 指纹识别。 这是一种利用CSS的特性来识别用户,即使他们清除了cookie或者使用了隐私模式。 我们将深入探讨这种技术的原理、实现方式,以及它所带来的伦理和社会问题。

什么是CSS指纹识别?

传统的用户追踪方式,例如 cookies 或 localStorage,容易被用户清除或禁用。 CSS 指纹识别则利用了浏览器渲染网页时的一些细微差别,这些差别来自于用户的操作系统、浏览器设置、以及安装的字体等。 通过收集这些信息,我们可以创建一个几乎唯一的“指纹”,用于识别用户。

CSS 指纹识别并非百分之百准确,但它可以与其他指纹识别技术结合使用,提高识别的准确率。

CSS指纹识别的原理

CSS指纹识别的核心在于,不同的浏览器和操作系统对CSS的解析和渲染可能存在细微的差异。这些差异可以体现在以下几个方面:

  1. 媒体查询 (Media Queries): 不同的设备和浏览器对媒体查询的支持程度和解析方式可能不同。例如,不同设备的分辨率、像素密度、设备方向等信息都可以通过媒体查询获取。

  2. 系统字体列表 (System Font List): 不同的操作系统和设备安装的字体不同。我们可以通过CSS来检测用户是否安装了特定的字体。

  3. CSS 属性支持: 不同的浏览器对CSS属性的支持程度不同。例如,某些浏览器可能不支持某些CSS3属性。

  4. 渲染差异: 即使在相同的浏览器和操作系统下,由于硬件加速、图形驱动等因素,页面渲染也可能存在细微的差异。 这部分更复杂,涉及到 canvas 指纹识别等技术,我们今天重点关注前两点。

利用媒体查询生成指纹

媒体查询允许我们根据设备的特性应用不同的CSS样式。 通过精心设计的媒体查询,我们可以收集用户的设备信息,并将其用于生成指纹。

1. 获取屏幕分辨率:

我们可以使用 widthheight 媒体特性来获取屏幕的分辨率。

@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) 中使用。

服务器端处理:

在服务器端,我们需要存储和分析指纹数据。 可以使用数据库来存储指纹,并使用算法来比较不同的指纹,从而识别用户。

指纹识别的流程:

  1. 客户端生成媒体查询指纹和字体指纹。
  2. 客户端将两个指纹合并,并进行哈希处理。
  3. 客户端将哈希后的指纹发送到服务器端。
  4. 服务器端在数据库中查找匹配的指纹。
  5. 如果找到匹配的指纹,则识别用户。 否则,将新的指纹添加到数据库中。

伦理和社会问题

CSS 指纹识别技术引发了一系列的伦理和社会问题。

  • 隐私侵犯: 用户在不知情的情况下被追踪,侵犯了用户的隐私权。
  • 透明度: 用户很难检测和阻止 CSS 指纹识别。
  • 滥用: 该技术可能被用于恶意目的,例如定向广告、价格歧视等。

我们需要在使用 CSS 指纹识别技术时保持谨慎,并充分考虑其可能带来的负面影响。

防御 CSS 指纹识别

用户可以采取一些措施来防御 CSS 指纹识别。

  • 禁用 JavaScript: 禁用 JavaScript 可以阻止脚本收集媒体查询和字体信息。 然而,这会影响网站的正常功能。
  • 使用隐私浏览器: 一些隐私浏览器,例如 Brave 和 Tor,可以阻止指纹识别。
  • 使用浏览器扩展: 有一些浏览器扩展可以随机化指纹信息,从而阻止指纹识别。 例如,Privacy Badger 和 NoScript。
  • 定期清除浏览器数据: 清除浏览器缓存、cookies 和历史记录可以减少指纹的唯一性。
  • 安装相同的字体: 安装常见的字体可以减少字体指纹的唯一性。
  • 使用虚拟机: 在虚拟机中浏览网页可以隔离指纹信息。

以下是一个表格,总结了防御 CSS 指纹识别的常见方法:

防御方法 优点 缺点
禁用 JavaScript 阻止脚本收集信息 影响网站的正常功能
使用隐私浏览器 自动阻止指纹识别 可能不兼容某些网站
使用浏览器扩展 可以随机化指纹信息 可能影响浏览器性能
定期清除浏览器数据 减少指纹的唯一性 每次清除后都需要重新登录网站
安装相同的字体 减少字体指纹的唯一性 需要手动安装字体
使用虚拟机 隔离指纹信息 性能开销大

总结

我们今天探讨了 CSS 指纹识别的原理、实现方式,以及它所带来的伦理和社会问题。 虽然这种技术可以用于识别用户,但也存在着隐私侵犯的风险。 用户可以采取一些措施来防御 CSS 指纹识别,例如禁用 JavaScript、使用隐私浏览器、使用浏览器扩展等。 我们需要在使用 CSS 指纹识别技术时保持谨慎,并充分考虑其可能带来的负面影响。

媒体查询可以获取设备分辨率和像素密度,系统字体列表可以检测用户安装的字体。 结合使用这两种技术可以生成更精确的用户指纹,但需要谨慎使用以避免侵犯用户隐私。 保护用户隐私是开发者的责任,我们需要在技术发展的同时,也要关注伦理和社会问题。

更多IT精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注