likes
comments
collection
share

FingerprintJS中有意思的知识

作者站长头像
站长
· 阅读数 20

FingerprintJS

之前有后端小哥问道,前端可否生成浏览器唯一标识UUID,发给后端处理。当时我想都不用想,直接拒绝(手动狗头)。后来想了想,这是个不错了研究题材,就google了一把,于是乎发现了这个宝藏库(FingerprintJS)。它能够生成浏览器唯一标识来区别不同的用户,pro版本的识别率高达99.5%(然而开源版本只有60%,本文简述的是开源版本)。

FingerprintJS 原理

FingerprintJS 的实现思路挺简单的,首先找出能区分浏览器不同的部分,我们最常见的就是通过navigator来获取浏览器的详细信息了。而 FingerprintJS 除了使用 navigator, 还用到了canvas指纹,字体宽高等其他方面,具体看下面代码:

// FingerprintJS 细化了各个不同的部分
export const sources = {
  // Expected errors and default values must be handled inside the functions. Unexpected errors must be thrown.
  osCpu: getOsCpu,
  languages: getLanguages,
  colorDepth: getColorDepth,
  deviceMemory: getDeviceMemory,
  screenResolution: getScreenResolution,
  availableScreenResolution: getAvailableScreenResolution,
  hardwareConcurrency: getHardwareConcurrency,
  timezoneOffset: getTimezoneOffset,
  timezone: getTimezone,
  sessionStorage: getSessionStorage,
  localStorage: getLocalStorage,
  indexedDB: getIndexedDB,
  openDatabase: getOpenDatabase,
  cpuClass: getCpuClass,
  platform: getPlatform,
  plugins: getPlugins,
  canvas: getCanvasFingerprint,
  // adBlock: isAdblockUsed, // https://github.com/fingerprintjs/fingerprintjs/issues/405
  touchSupport: getTouchSupport,
  fonts: getFonts,
  audio: getAudioFingerprint,
  pluginsSupport: getPluginsSupport,
  productSub: getProductSub,
  emptyEvalLength: getEmptyEvalLength,
  errorFF: getErrorFF,
  vendor: getVendor,
  chrome: getChrome,
  cookiesEnabled: areCookiesEnabled,
}

FingerprintJS 把各个部分当成组件分别实现,然后使用loadBuiltinSources方法获取所有组件,接着使用componentsToCanonicalString遍历组件的返回值,系列化拼接成字符串,最后用x64hash128方法把字符串转成一个独一无二的hash值。

上面的一大坨组件,其实大部分都是查询navigator对象的,这里就不展开了,除此之外,canvas指纹,字体差和所用到了hash算法倒是有点意思。

canvas指纹

canvas指纹是指:通过canvas接口在页面上绘制一个隐藏的图像,在不同的系统,浏览器中最终的图像是存在像素级别的差别的,主要是因为操作系统各自使用了不同的设置和算法来进行抗锯齿和子像素渲染操作。即使相同的绘图操作,产生的图片数据的CRC检验也不相同。(CRC是指使用canvas.toDataURL返回的base64数据中,最后一段32位的验证码)。

canvas指纹并不少见,除此之外,还有使用音频指纹,甚至使用WebGL指纹和WebGL指纹的。 音频指纹跟canvas指纹的原理差不多,都是利用硬件或软件的差异,前者生成音频,后者生成图片,然后计算得到不同哈希值来作为标识。音频指纹的生成方式有两种:

  1. 生成音频信息流(三角波),对其进行FFT变换,计算SHA值作为指纹
  2. 生成音频信息流(正弦波),进行动态压缩处理,计算MD5值 FingerprintJS 也使用了音频指纹,使用的貌似是方法一,具体有兴趣可以看一下代码

系统字体

FingerprintJS 通过getFonts()方法获取系统支持的字体,原理不复杂,大致分为下面的步骤:

  1. 定义字体文案const testString = 'mmMwWLliI0O&1';字体大小const textSize = '48px';基本的fontFamilyconst baseFonts = ['monospace', 'sans-serif', 'serif'];和待检测字体列表fontList(定义了很多字体类型)
  2. 分别遍历baseFonts和fontList,生成span标签,并设置对应的fontFamily,注意fontList遍历的时候,需要同时遍历baseFonts,这样设置fontFamily的时候可以设置默认的baseFonts字体
 // creates a span and load the font to detect and a base font for fallback
const createSpanWithFonts = (fontToDetect: string, baseFont: string) => {
  return createSpan(`'${fontToDetect}',${baseFont}`)
}

  1. 比较fontList和baseFonts的字体文案在不同的fontFamily下的宽高,如果不相等说明支持该字体,如果相等,说明系统不支持fontList的字体,使用了默认的baseFonts的字体
// checks if a font is available
const isFontAvailable = (fontSpans: HTMLElement[]) => {
  return baseFonts.some(
    (baseFont, baseFontIndex) =>
      fontSpans[baseFontIndex].offsetWidth !== defaultWidth[baseFont] ||
      fontSpans[baseFontIndex].offsetHeight !== defaultHeight[baseFont],
  )
}

murmurHash3

x64hash128使用的hash算法是murmurHash3。代码比较复杂,有兴趣可以去琢磨一下,这里只做简单介绍。

MurmurHash是一种经过广泛测试且速度很快的非加密哈希函数。它有Austin Appleby于2008年创建,并存在多种变体,名字来自两个基本运算,即multiply(乘法)和rotate(旋转)(尽管该算法实际上使用shift和xor而不是rotate)。

MurmurHash3可以产生32位或128位哈希,旧版本MurmurHash2产生32位或64位值,MurmurHash2A变体添加了Merkel-Damgard构造,以便可以逐步调用它。MurmurHash64A针对64位处理器进行了优化,针对32位处理器进行MurmurHash64B优化

未完待续