Реализация Device Fingerprinting для обнаружения злоупотреблений

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация Device Fingerprinting для обнаружения злоупотреблений
Сложная
~3-5 рабочих дней
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

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 рабочих дней.