This commit is contained in:
Professional 2025-05-26 18:00:56 +07:00
parent 2317ecafca
commit e8c839d493
4 changed files with 212 additions and 76 deletions

View File

@ -18,23 +18,28 @@ app.use(router);
// Получаем экземпляр нашего auth "стора" // Получаем экземпляр нашего auth "стора"
const { fetchUser, handleDeviceBlocked } = useAuth(); const { fetchUser, handleDeviceBlocked } = useAuth();
// Проверка на наличие браузерного окружения
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
// Асинхронная самовызывающаяся функция для инициализации // Асинхронная самовызывающаяся функция для инициализации
(async () => { (async () => {
try { try {
// Инициализируем систему безопасности устройств // Инициализируем систему безопасности устройств только в браузере
console.log('Инициализация системы безопасности устройств...'); if (isBrowser) {
await deviceSecurityService.initialize(); console.log('Инициализация системы безопасности устройств...');
await deviceSecurityService.initialize();
// Подписываемся на события блокировки устройства // Подписываемся на события блокировки устройства
deviceSecurityService.onDeviceBlocked(handleDeviceBlocked); deviceSecurityService.onDeviceBlocked(handleDeviceBlocked);
console.log('Система безопасности устройств инициализирована'); console.log('Система безопасности устройств инициализирована');
// Проверяем, не заблокировано ли устройство // Проверяем, не заблокировано ли устройство
if (deviceSecurityService.isDeviceBlocked()) { if (deviceSecurityService.isDeviceBlocked()) {
console.warn('Устройство заблокировано, перенаправляем на страницу входа'); console.warn('Устройство заблокировано, перенаправляем на страницу входа');
window.location.href = '/login?blocked=true&type=device'; window.location.href = '/login?blocked=true&type=device';
return; return;
}
} }
await fetchUser(); // Пытаемся загрузить пользователя по токену из localStorage await fetchUser(); // Пытаемся загрузить пользователя по токену из localStorage

View File

@ -1,32 +1,48 @@
import DeviceFingerprint from '../utils/deviceFingerprint.js'; import DeviceFingerprint from '../utils/deviceFingerprint.js';
import { api } from './api.js'; import api from './api.js';
// Проверка на наличие браузерного окружения
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
/** /**
* Сервис для работы с системой блокировки устройств * Сервис для работы с системой блокировки устройств
*/ */
class DeviceSecurityService { class DeviceSecurityService {
constructor() { constructor() {
this.deviceFingerprint = new DeviceFingerprint(); // Инициализируем только в браузерном окружении
this.currentFingerprint = null; if (isBrowser) {
this.isBlocked = false; this.deviceFingerprint = new DeviceFingerprint();
this.initialized = false; this.currentFingerprint = null;
this.suspiciousActivities = []; this.isBlocked = false;
this.lastFingerprintCheck = null; this.initialized = false;
this.checkInterval = null; this.suspiciousActivities = [];
this.lastFingerprintCheck = null;
this.checkInterval = null;
// Настройки мониторинга // Настройки мониторинга
this.config = { this.config = {
checkIntervalMs: 30000, // Проверка каждые 30 секунд checkIntervalMs: 30000, // Проверка каждые 30 секунд
maxSuspiciousActivities: 10, maxSuspiciousActivities: 10,
reportThreshold: 5, // Отправлять отчет после 5 подозрительных активностей reportThreshold: 5, // Отправлять отчет после 5 подозрительных активностей
fingerprintChangeThreshold: 0.1 // Порог изменения отпечатка (10%) fingerprintChangeThreshold: 0.1 // Порог изменения отпечатка (10%)
}; };
}
} }
/** /**
* Инициализация сервиса безопасности * Инициализация сервиса безопасности
*/ */
async initialize() { async initialize() {
// Проверяем, находимся ли мы в браузерном окружении
if (!isBrowser) {
console.log('[DeviceSecurity] Инициализация пропущена - не браузерное окружение');
return {
fingerprint: null,
isBlocked: false,
initialized: false
};
}
try { try {
console.log('[DeviceSecurity] Инициализация сервиса безопасности устройств...'); console.log('[DeviceSecurity] Инициализация сервиса безопасности устройств...');
@ -58,6 +74,8 @@ class DeviceSecurityService {
* Проверка статуса блокировки устройства * Проверка статуса блокировки устройства
*/ */
async checkDeviceBlockStatus() { async checkDeviceBlockStatus() {
if (!isBrowser) return true;
try { try {
const response = await api.post('/security/check-device', { const response = await api.post('/security/check-device', {
fingerprint: this.currentFingerprint, fingerprint: this.currentFingerprint,
@ -94,6 +112,8 @@ class DeviceSecurityService {
* Получение информации об устройстве * Получение информации об устройстве
*/ */
getDeviceInfo() { getDeviceInfo() {
if (!isBrowser) return {};
return { return {
userAgent: navigator.userAgent, userAgent: navigator.userAgent,
platform: navigator.platform, platform: navigator.platform,
@ -109,6 +129,8 @@ class DeviceSecurityService {
* Получение количества посещений (из localStorage) * Получение количества посещений (из localStorage)
*/ */
getVisitCount() { getVisitCount() {
if (!isBrowser) return 1;
try { try {
const visits = localStorage.getItem('device_visits'); const visits = localStorage.getItem('device_visits');
const count = visits ? parseInt(visits) + 1 : 1; const count = visits ? parseInt(visits) + 1 : 1;
@ -123,6 +145,8 @@ class DeviceSecurityService {
* Запуск периодического мониторинга * Запуск периодического мониторинга
*/ */
startMonitoring() { startMonitoring() {
if (!isBrowser) return;
if (this.checkInterval) { if (this.checkInterval) {
clearInterval(this.checkInterval); clearInterval(this.checkInterval);
} }
@ -142,6 +166,8 @@ class DeviceSecurityService {
* Остановка мониторинга * Остановка мониторинга
*/ */
stopMonitoring() { stopMonitoring() {
if (!isBrowser) return;
if (this.checkInterval) { if (this.checkInterval) {
clearInterval(this.checkInterval); clearInterval(this.checkInterval);
this.checkInterval = null; this.checkInterval = null;
@ -153,6 +179,8 @@ class DeviceSecurityService {
* Выполнение периодической проверки безопасности * Выполнение периодической проверки безопасности
*/ */
async performSecurityCheck() { async performSecurityCheck() {
if (!isBrowser) return;
try { try {
// Проверяем, не изменился ли отпечаток устройства // Проверяем, не изменился ли отпечаток устройства
await this.checkFingerprintChanges(); await this.checkFingerprintChanges();
@ -382,45 +410,9 @@ class DeviceSecurityService {
* Проверка блокировки * Проверка блокировки
*/ */
isDeviceBlocked() { isDeviceBlocked() {
if (!isBrowser) return false;
return this.isBlocked; return this.isBlocked;
} }
/**
* Получение компонентов отпечатка (для отладки)
*/
getFingerprintComponents() {
return this.deviceFingerprint.getComponents();
}
/**
* Получение статистики подозрительных активностей
*/
getSuspiciousActivitiesStats() {
return {
total: this.suspiciousActivities.length,
activities: this.suspiciousActivities.slice(),
lastCheck: this.lastFingerprintCheck
};
}
/**
* Очистка истории (для отладки)
*/
clearHistory() {
this.suspiciousActivities = [];
localStorage.removeItem('login_attempts');
console.log('[DeviceSecurity] История очищена');
}
/**
* Деструктор
*/
destroy() {
this.stopMonitoring();
this.suspiciousActivities = [];
this.initialized = false;
console.log('[DeviceSecurity] Сервис уничтожен');
}
} }
// Создаем единственный экземпляр сервиса // Создаем единственный экземпляр сервиса

View File

@ -3,6 +3,9 @@
* Используется для блокировки на уровне железа * Используется для блокировки на уровне железа
*/ */
// Проверка на наличие браузерного окружения
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
class DeviceFingerprint { class DeviceFingerprint {
constructor() { constructor() {
this.components = new Map(); this.components = new Map();
@ -14,6 +17,14 @@ class DeviceFingerprint {
* Инициализация и создание отпечатка устройства * Инициализация и создание отпечатка устройства
*/ */
async initialize() { async initialize() {
// Если мы не в браузере - возвращаем фиктивный отпечаток
if (!isBrowser) {
console.log('[DeviceFingerprint] Не браузерное окружение, отпечаток не создаётся');
this.fingerprint = 'server-environment-no-fingerprint';
this.initialized = true;
return this.fingerprint;
}
try { try {
console.log('[DeviceFingerprint] Начинаем инициализацию отпечатка устройства...'); console.log('[DeviceFingerprint] Начинаем инициализацию отпечатка устройства...');
@ -53,6 +64,8 @@ class DeviceFingerprint {
* Информация об экране * Информация об экране
*/ */
async collectScreenInfo() { async collectScreenInfo() {
if (!isBrowser) return;
try { try {
this.components.set('screen', { this.components.set('screen', {
width: screen.width, width: screen.width,
@ -72,6 +85,8 @@ class DeviceFingerprint {
* Информация о навигаторе * Информация о навигаторе
*/ */
async collectNavigatorInfo() { async collectNavigatorInfo() {
if (!isBrowser) return;
try { try {
this.components.set('navigator', { this.components.set('navigator', {
userAgent: navigator.userAgent, userAgent: navigator.userAgent,
@ -93,6 +108,8 @@ class DeviceFingerprint {
* Canvas fingerprinting * Canvas fingerprinting
*/ */
async collectCanvasFingerprint() { async collectCanvasFingerprint() {
if (!isBrowser) return;
try { try {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
@ -116,7 +133,7 @@ class DeviceFingerprint {
ctx.fill(); ctx.fill();
const canvasData = canvas.toDataURL(); const canvasData = canvas.toDataURL();
this.components.set('canvas', this.hashString(canvasData)); this.components.set('canvas', await this.hashString(canvasData));
} catch (e) { } catch (e) {
console.warn('[DeviceFingerprint] Не удалось создать canvas fingerprint:', e); console.warn('[DeviceFingerprint] Не удалось создать canvas fingerprint:', e);
@ -127,6 +144,8 @@ class DeviceFingerprint {
* WebGL fingerprinting * WebGL fingerprinting
*/ */
async collectWebGLFingerprint() { async collectWebGLFingerprint() {
if (!isBrowser) return;
try { try {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
@ -152,6 +171,8 @@ class DeviceFingerprint {
* Audio fingerprinting * Audio fingerprinting
*/ */
async collectAudioFingerprint() { async collectAudioFingerprint() {
if (!isBrowser) return Promise.resolve();
return new Promise((resolve) => { return new Promise((resolve) => {
try { try {
if (!window.AudioContext && !window.webkitAudioContext) { if (!window.AudioContext && !window.webkitAudioContext) {
@ -177,11 +198,11 @@ class DeviceFingerprint {
scriptProcessor.connect(gainNode); scriptProcessor.connect(gainNode);
gainNode.connect(context.destination); gainNode.connect(context.destination);
scriptProcessor.onaudioprocess = (event) => { scriptProcessor.onaudioprocess = async (event) => {
const bins = new Float32Array(analyser.frequencyBinCount); const bins = new Float32Array(analyser.frequencyBinCount);
analyser.getFloatFrequencyData(bins); analyser.getFloatFrequencyData(bins);
const audioHash = this.hashString(bins.slice(0, 50).join(',')); const audioHash = await this.hashString(bins.slice(0, 50).join(','));
this.components.set('audio', audioHash); this.components.set('audio', audioHash);
oscillator.disconnect(); oscillator.disconnect();
@ -211,6 +232,8 @@ class DeviceFingerprint {
* Информация о временной зоне * Информация о временной зоне
*/ */
async collectTimezoneInfo() { async collectTimezoneInfo() {
if (!isBrowser) return;
try { try {
this.components.set('timezone', { this.components.set('timezone', {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
@ -226,6 +249,8 @@ class DeviceFingerprint {
* Языковые настройки * Языковые настройки
*/ */
async collectLanguageInfo() { async collectLanguageInfo() {
if (!isBrowser) return;
try { try {
this.components.set('language', { this.components.set('language', {
language: navigator.language, language: navigator.language,
@ -240,6 +265,8 @@ class DeviceFingerprint {
* Платформа и ОС * Платформа и ОС
*/ */
async collectPlatformInfo() { async collectPlatformInfo() {
if (!isBrowser) return;
try { try {
this.components.set('platform', { this.components.set('platform', {
platform: navigator.platform, platform: navigator.platform,
@ -255,6 +282,8 @@ class DeviceFingerprint {
* Аппаратная информация * Аппаратная информация
*/ */
async collectHardwareInfo() { async collectHardwareInfo() {
if (!isBrowser) return;
try { try {
this.components.set('hardware', { this.components.set('hardware', {
hardwareConcurrency: navigator.hardwareConcurrency || 0, hardwareConcurrency: navigator.hardwareConcurrency || 0,
@ -270,6 +299,8 @@ class DeviceFingerprint {
* Информация о хранилище * Информация о хранилище
*/ */
async collectStorageInfo() { async collectStorageInfo() {
if (!isBrowser) return;
try { try {
let storageQuota = 0; let storageQuota = 0;
if ('storage' in navigator && 'estimate' in navigator.storage) { if ('storage' in navigator && 'estimate' in navigator.storage) {
@ -292,6 +323,8 @@ class DeviceFingerprint {
* Сетевая информация * Сетевая информация
*/ */
async collectNetworkInfo() { async collectNetworkInfo() {
if (!isBrowser) return;
try { try {
const networkInfo = {}; const networkInfo = {};
@ -312,6 +345,8 @@ class DeviceFingerprint {
* Генерация финального отпечатка * Генерация финального отпечатка
*/ */
async generateFingerprint() { async generateFingerprint() {
if (!isBrowser) return 'server-environment-no-fingerprint';
const componentsString = JSON.stringify(Array.from(this.components.entries()).sort()); const componentsString = JSON.stringify(Array.from(this.components.entries()).sort());
return await this.hashString(componentsString); return await this.hashString(componentsString);
} }
@ -320,6 +355,8 @@ class DeviceFingerprint {
* Создание резервного отпечатка при ошибках * Создание резервного отпечатка при ошибках
*/ */
async createFallbackFingerprint() { async createFallbackFingerprint() {
if (!isBrowser) return 'server-environment-no-fingerprint';
const fallbackData = { const fallbackData = {
userAgent: navigator.userAgent, userAgent: navigator.userAgent,
screen: `${screen.width}x${screen.height}`, screen: `${screen.width}x${screen.height}`,
@ -335,12 +372,19 @@ class DeviceFingerprint {
* Хеширование строки * Хеширование строки
*/ */
async hashString(str) { async hashString(str) {
if (crypto && crypto.subtle) { if (!isBrowser) return 'server-environment-hash';
const encoder = new TextEncoder();
const data = encoder.encode(str); if (window.crypto && window.crypto.subtle) {
const hashBuffer = await crypto.subtle.digest('SHA-256', data); try {
const hashArray = Array.from(new Uint8Array(hashBuffer)); const encoder = new TextEncoder();
return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); const data = encoder.encode(str);
const hashBuffer = await window.crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
} catch (e) {
console.warn('[DeviceFingerprint] Ошибка при использовании Crypto API:', e);
return this.simpleHash(str);
}
} else { } else {
// Fallback для старых браузеров // Fallback для старых браузеров
return this.simpleHash(str); return this.simpleHash(str);

View File

@ -0,0 +1,95 @@
<template>
<div class="device-blocked-container">
<div class="blocked-card">
<div class="blocked-icon">
<i class="fas fa-ban"></i>
</div>
<h1>Устройство заблокировано</h1>
<p>
Для безопасности вашей учетной записи, это устройство было заблокировано администратором.
</p>
<p class="device-info" v-if="fingerprint">
ID устройства: <code>{{ shortenFingerprint(fingerprint) }}</code>
</p>
<p class="contact-info">
Если вы считаете, что это ошибка, пожалуйста, свяжитесь с администратором сервиса.
</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { useAuth } from '../auth';
const { deviceFingerprint } = useAuth();
const fingerprint = ref('');
onMounted(() => {
fingerprint.value = deviceFingerprint.value || localStorage.getItem('deviceFingerprint') || '';
});
const shortenFingerprint = (fp) => {
if (!fp) return '';
if (fp.length <= 16) return fp;
return fp.substring(0, 8) + '...' + fp.substring(fp.length - 8);
};
</script>
<style scoped>
.device-blocked-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
background-color: #f8f9fa;
}
.blocked-card {
max-width: 500px;
padding: 30px;
border-radius: 10px;
background-color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
text-align: center;
}
.blocked-icon {
font-size: 64px;
color: #dc3545;
margin-bottom: 20px;
}
h1 {
color: #dc3545;
margin-bottom: 20px;
}
p {
margin-bottom: 15px;
color: #555;
font-size: 16px;
line-height: 1.5;
}
.device-info {
background-color: #f8f9fa;
padding: 10px;
border-radius: 5px;
margin: 20px 0;
}
code {
font-family: monospace;
background-color: #eee;
padding: 3px 5px;
border-radius: 3px;
font-size: 14px;
}
.contact-info {
font-style: italic;
color: #666;
}
</style>