Device Fingerprinting для обнаружения злоупотреблений
Device fingerprinting — идентификация устройства по совокупности характеристик браузера и ОС без использования cookies. Позволяет распознать мошенника после смены IP, сброса cookies и даже в инкогнито-режиме. В отличие от cookies не требует явного согласия для security-целей по большинству законодательств (не является рекламным трекингом).
Компоненты fingerprint
Качество fingerprint определяется энтропией — количеством информации каждого атрибута:
| Компонент | Энтропия (бит) | Описание |
|---|---|---|
| User-Agent | 10–14 | ОС + браузер + версия |
| Canvas fingerprint | 8–12 | GPU + шрифты + AA рендеринг |
| WebGL vendor/renderer | 8–10 | GPU модель |
| Установленные шрифты | 6–10 | Список через measureText |
| AudioContext fingerprint | 6–8 | DSP характеристики железа |
| Разрешение + devicePixelRatio | 4–6 | Монитор + масштаб |
| Timezone | 4–5 | IANA timezone |
| Languages | 3–4 | Список языков браузера |
| Platform | 2–3 | Windows/Mac/Linux |
| CPU cores, memory | 3–4 | navigator.hardwareConcurrency |
Комбинация даёт 40–80 бит энтропии — теоретически достаточно для глобальной уникальности.
Клиентский сбор (JavaScript)
class DeviceFingerprinter {
async collect() {
const [canvas, webgl, audio, fonts] = await Promise.all([
this.getCanvasFingerprint(),
this.getWebGLFingerprint(),
this.getAudioFingerprint(),
this.getInstalledFonts(),
])
return {
// Базовые
userAgent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language,
languages: navigator.languages?.join(',') ?? '',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
// Экран
screenResolution: `${screen.width}x${screen.height}`,
screenDepth: screen.colorDepth,
devicePixelRatio: window.devicePixelRatio,
windowSize: `${window.innerWidth}x${window.innerHeight}`,
// Железо
cpuCores: navigator.hardwareConcurrency,
deviceMemory: navigator.deviceMemory,
// Отпечатки рендеринга
canvas,
webgl,
audio,
fonts: fonts.slice(0, 20).join(','), // топ-20 шрифтов
// Браузерные API
cookieEnabled: navigator.cookieEnabled,
doNotTrack: navigator.doNotTrack,
touchPoints: navigator.maxTouchPoints,
pdfViewer: navigator.pdfViewerEnabled,
}
}
async getCanvasFingerprint() {
const canvas = document.createElement('canvas')
canvas.width = 200
canvas.height = 50
const ctx = canvas.getContext('2d')
// Рисуем текст с эффектами — каждый GPU рендерит по-своему
ctx.textBaseline = 'top'
ctx.font = '14px Arial'
ctx.fillStyle = '#f60'
ctx.fillRect(125, 1, 62, 20)
ctx.fillStyle = '#069'
ctx.fillText('FP Test 🦄 ', 2, 15)
ctx.fillStyle = 'rgba(102, 204, 0, 0.7)'
ctx.fillText('FP Test 🦄 ', 4, 17)
return canvas.toDataURL()
.split(',')[1]
.substring(0, 50) // берём первые 50 символов — достаточно для энтропии
}
async getWebGLFingerprint() {
const canvas = document.createElement('canvas')
const gl = canvas.getContext('webgl') ||
canvas.getContext('experimental-webgl')
if (!gl) return 'no_webgl'
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info')
const vendor = debugInfo
? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL)
: gl.getParameter(gl.VENDOR)
const renderer = debugInfo
? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)
: gl.getParameter(gl.RENDERER)
return `${vendor}~${renderer}`
}
async getAudioFingerprint() {
try {
const ctx = new (window.AudioContext || window.webkitAudioContext)()
const oscillator = ctx.createOscillator()
const analyser = ctx.createAnalyser()
const gain = ctx.createGain()
gain.gain.value = 0
oscillator.connect(analyser)
analyser.connect(gain)
gain.connect(ctx.destination)
oscillator.start(0)
const buf = new Float32Array(analyser.frequencyBinCount)
analyser.getFloatFrequencyData(buf)
oscillator.stop()
ctx.close()
// Хеш массива частот
return buf.slice(0, 10).join(',')
} catch {
return 'no_audio'
}
}
async getInstalledFonts() {
// Метод measureText: сравниваем ширину текста в разных шрифтах
const baseline = this._measureFont('monospace')
const fonts = [
'Arial', 'Verdana', 'Helvetica', 'Times New Roman', 'Courier New',
'Georgia', 'Palatino', 'Garamond', 'Comic Sans MS', 'Trebuchet MS',
'Arial Black', 'Impact', 'Tahoma', 'Geneva', 'Lucida Console',
]
return fonts.filter(font =>
this._measureFont(font) !== baseline
)
}
_measureFont(font) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
ctx.font = `72px ${font}, monospace`
return ctx.measureText('mmmmmmmmmml').width
}
async getHash(data) {
const str = JSON.stringify(data)
const buf = new TextEncoder().encode(str)
const hashBuf = await crypto.subtle.digest('SHA-256', buf)
const hashArr = Array.from(new Uint8Array(hashBuf))
return hashArr.map(b => b.toString(16).padStart(2, '0')).join('')
}
}
// Использование
const fp = new DeviceFingerprinter()
const components = await fp.collect()
const visitorId = await fp.getHash(components)
// Отправить на сервер при логине/регистрации/оплате
fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...formData, device_fp: visitorId, fp_components: components })
})
FingerprintJS Pro (готовая библиотека)
import FingerprintJS from '@fingerprintjs/fingerprintjs-pro'
// Точность 99.5% против 40-60% у open-source версии
const fp = await FingerprintJS.load({ apiKey: 'YOUR_API_KEY' })
const result = await fp.get()
// result.visitorId — стабильный ID устройства
// result.confidence.score — уверенность 0-1
// result.incognito — режим инкогнито обнаружен
Серверная обработка и база данных
class DeviceFingerprintService:
def __init__(self, db, redis):
self.db = db
self.r = redis
def register_device(self, visitor_id: str, user_id: int, context: dict):
"""Связать fingerprint с пользователем"""
# Сохранить в БД
self.db.execute("""
INSERT INTO device_fingerprints
(visitor_id, user_id, ip, user_agent, components, created_at, last_seen_at)
VALUES (%s, %s, %s, %s, %s::jsonb, NOW(), NOW())
ON CONFLICT (visitor_id) DO UPDATE
SET last_seen_at = NOW(),
user_agent = EXCLUDED.user_agent
""", (
visitor_id,
user_id,
context['ip'],
context['user_agent'],
json.dumps(context.get('components', {}))
))
def check_device(self, visitor_id: str, user_id: int) -> dict:
"""Проверить соответствие fingerprint и пользователя"""
# Известно ли это устройство пользователю?
known = self.db.query_one("""
SELECT COUNT(*) as cnt FROM device_fingerprints
WHERE visitor_id = %s AND user_id = %s
""", (visitor_id, user_id))
if known and known['cnt'] > 0:
return {'status': 'known', 'trust': 'high'}
# Это устройство видели с другими аккаунтами?
other_users = self.db.query("""
SELECT DISTINCT user_id FROM device_fingerprints
WHERE visitor_id = %s AND user_id != %s
""", (visitor_id, user_id))
if other_users:
return {
'status': 'suspicious',
'trust': 'low',
'reason': f'device_shared_with_{len(other_users)}_accounts'
}
return {'status': 'new', 'trust': 'medium'}
def is_fraud_device(self, visitor_id: str) -> bool:
"""Проверить по базе известных мошеннических устройств"""
return bool(self.r.get(f"fraud_device:{visitor_id}"))
def mark_fraud(self, visitor_id: str, reason: str):
"""Пометить устройство как мошенническое"""
self.r.setex(f"fraud_device:{visitor_id}", 86400 * 90, reason)
# Заморозить все аккаунты с этого устройства
affected_users = self.db.query("""
SELECT DISTINCT user_id FROM device_fingerprints
WHERE visitor_id = %s
""", (visitor_id,))
for user in affected_users:
self.r.setex(f"account_flagged:{user['user_id']}", 86400, 'fraud_device')
Bypassing resistance (защита от обхода)
Fingerprinting можно обойти инструментами вроде Canvas Blocker или Privacy Badger. Контрмеры:
def check_fp_consistency(components: dict, header_ua: str) -> bool:
"""Проверить согласованность fingerprint с заголовками"""
fp_ua = components.get('userAgent', '')
# FP должен совпадать с HTTP заголовком
if fp_ua != header_ua:
return False # Манипуляция с fingerprint
# Canvas fingerprint должен быть непустым
canvas = components.get('canvas', '')
if not canvas or canvas == 'data:,':
return False # Canvas заблокирован
# WebGL должен возвращать реальный рендерер
webgl = components.get('webgl', '')
if webgl in ['', 'no_webgl', 'Google SwiftShader']:
# SwiftShader — признак виртуальной машины или VPN-браузера
pass # Повысить score риска, но не блокировать
return True
Срок выполнения
Реализация серверного + клиентского device fingerprinting с интеграцией в систему обнаружения злоупотреблений — 3–5 рабочих дней.







