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

View File

@ -1,32 +1,48 @@
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 {
constructor() {
this.deviceFingerprint = new DeviceFingerprint();
this.currentFingerprint = null;
this.isBlocked = false;
this.initialized = false;
this.suspiciousActivities = [];
this.lastFingerprintCheck = null;
this.checkInterval = null;
// Настройки мониторинга
this.config = {
checkIntervalMs: 30000, // Проверка каждые 30 секунд
maxSuspiciousActivities: 10,
reportThreshold: 5, // Отправлять отчет после 5 подозрительных активностей
fingerprintChangeThreshold: 0.1 // Порог изменения отпечатка (10%)
};
// Инициализируем только в браузерном окружении
if (isBrowser) {
this.deviceFingerprint = new DeviceFingerprint();
this.currentFingerprint = null;
this.isBlocked = false;
this.initialized = false;
this.suspiciousActivities = [];
this.lastFingerprintCheck = null;
this.checkInterval = null;
// Настройки мониторинга
this.config = {
checkIntervalMs: 30000, // Проверка каждые 30 секунд
maxSuspiciousActivities: 10,
reportThreshold: 5, // Отправлять отчет после 5 подозрительных активностей
fingerprintChangeThreshold: 0.1 // Порог изменения отпечатка (10%)
};
}
}
/**
* Инициализация сервиса безопасности
*/
async initialize() {
// Проверяем, находимся ли мы в браузерном окружении
if (!isBrowser) {
console.log('[DeviceSecurity] Инициализация пропущена - не браузерное окружение');
return {
fingerprint: null,
isBlocked: false,
initialized: false
};
}
try {
console.log('[DeviceSecurity] Инициализация сервиса безопасности устройств...');
@ -58,6 +74,8 @@ class DeviceSecurityService {
* Проверка статуса блокировки устройства
*/
async checkDeviceBlockStatus() {
if (!isBrowser) return true;
try {
const response = await api.post('/security/check-device', {
fingerprint: this.currentFingerprint,
@ -94,6 +112,8 @@ class DeviceSecurityService {
* Получение информации об устройстве
*/
getDeviceInfo() {
if (!isBrowser) return {};
return {
userAgent: navigator.userAgent,
platform: navigator.platform,
@ -109,6 +129,8 @@ class DeviceSecurityService {
* Получение количества посещений (из localStorage)
*/
getVisitCount() {
if (!isBrowser) return 1;
try {
const visits = localStorage.getItem('device_visits');
const count = visits ? parseInt(visits) + 1 : 1;
@ -123,6 +145,8 @@ class DeviceSecurityService {
* Запуск периодического мониторинга
*/
startMonitoring() {
if (!isBrowser) return;
if (this.checkInterval) {
clearInterval(this.checkInterval);
}
@ -142,6 +166,8 @@ class DeviceSecurityService {
* Остановка мониторинга
*/
stopMonitoring() {
if (!isBrowser) return;
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
@ -153,6 +179,8 @@ class DeviceSecurityService {
* Выполнение периодической проверки безопасности
*/
async performSecurityCheck() {
if (!isBrowser) return;
try {
// Проверяем, не изменился ли отпечаток устройства
await this.checkFingerprintChanges();
@ -382,45 +410,9 @@ class DeviceSecurityService {
* Проверка блокировки
*/
isDeviceBlocked() {
if (!isBrowser) return false;
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 {
constructor() {
this.components = new Map();
@ -14,6 +17,14 @@ class DeviceFingerprint {
* Инициализация и создание отпечатка устройства
*/
async initialize() {
// Если мы не в браузере - возвращаем фиктивный отпечаток
if (!isBrowser) {
console.log('[DeviceFingerprint] Не браузерное окружение, отпечаток не создаётся');
this.fingerprint = 'server-environment-no-fingerprint';
this.initialized = true;
return this.fingerprint;
}
try {
console.log('[DeviceFingerprint] Начинаем инициализацию отпечатка устройства...');
@ -53,6 +64,8 @@ class DeviceFingerprint {
* Информация об экране
*/
async collectScreenInfo() {
if (!isBrowser) return;
try {
this.components.set('screen', {
width: screen.width,
@ -72,6 +85,8 @@ class DeviceFingerprint {
* Информация о навигаторе
*/
async collectNavigatorInfo() {
if (!isBrowser) return;
try {
this.components.set('navigator', {
userAgent: navigator.userAgent,
@ -93,6 +108,8 @@ class DeviceFingerprint {
* Canvas fingerprinting
*/
async collectCanvasFingerprint() {
if (!isBrowser) return;
try {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
@ -116,7 +133,7 @@ class DeviceFingerprint {
ctx.fill();
const canvasData = canvas.toDataURL();
this.components.set('canvas', this.hashString(canvasData));
this.components.set('canvas', await this.hashString(canvasData));
} catch (e) {
console.warn('[DeviceFingerprint] Не удалось создать canvas fingerprint:', e);
@ -127,6 +144,8 @@ class DeviceFingerprint {
* WebGL fingerprinting
*/
async collectWebGLFingerprint() {
if (!isBrowser) return;
try {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
@ -152,6 +171,8 @@ class DeviceFingerprint {
* Audio fingerprinting
*/
async collectAudioFingerprint() {
if (!isBrowser) return Promise.resolve();
return new Promise((resolve) => {
try {
if (!window.AudioContext && !window.webkitAudioContext) {
@ -177,11 +198,11 @@ class DeviceFingerprint {
scriptProcessor.connect(gainNode);
gainNode.connect(context.destination);
scriptProcessor.onaudioprocess = (event) => {
scriptProcessor.onaudioprocess = async (event) => {
const bins = new Float32Array(analyser.frequencyBinCount);
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);
oscillator.disconnect();
@ -211,6 +232,8 @@ class DeviceFingerprint {
* Информация о временной зоне
*/
async collectTimezoneInfo() {
if (!isBrowser) return;
try {
this.components.set('timezone', {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
@ -226,6 +249,8 @@ class DeviceFingerprint {
* Языковые настройки
*/
async collectLanguageInfo() {
if (!isBrowser) return;
try {
this.components.set('language', {
language: navigator.language,
@ -240,6 +265,8 @@ class DeviceFingerprint {
* Платформа и ОС
*/
async collectPlatformInfo() {
if (!isBrowser) return;
try {
this.components.set('platform', {
platform: navigator.platform,
@ -255,6 +282,8 @@ class DeviceFingerprint {
* Аппаратная информация
*/
async collectHardwareInfo() {
if (!isBrowser) return;
try {
this.components.set('hardware', {
hardwareConcurrency: navigator.hardwareConcurrency || 0,
@ -270,6 +299,8 @@ class DeviceFingerprint {
* Информация о хранилище
*/
async collectStorageInfo() {
if (!isBrowser) return;
try {
let storageQuota = 0;
if ('storage' in navigator && 'estimate' in navigator.storage) {
@ -292,6 +323,8 @@ class DeviceFingerprint {
* Сетевая информация
*/
async collectNetworkInfo() {
if (!isBrowser) return;
try {
const networkInfo = {};
@ -312,6 +345,8 @@ class DeviceFingerprint {
* Генерация финального отпечатка
*/
async generateFingerprint() {
if (!isBrowser) return 'server-environment-no-fingerprint';
const componentsString = JSON.stringify(Array.from(this.components.entries()).sort());
return await this.hashString(componentsString);
}
@ -320,6 +355,8 @@ class DeviceFingerprint {
* Создание резервного отпечатка при ошибках
*/
async createFallbackFingerprint() {
if (!isBrowser) return 'server-environment-no-fingerprint';
const fallbackData = {
userAgent: navigator.userAgent,
screen: `${screen.width}x${screen.height}`,
@ -335,12 +372,19 @@ class DeviceFingerprint {
* Хеширование строки
*/
async hashString(str) {
if (crypto && crypto.subtle) {
const encoder = new TextEncoder();
const data = encoder.encode(str);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
if (!isBrowser) return 'server-environment-hash';
if (window.crypto && window.crypto.subtle) {
try {
const encoder = new TextEncoder();
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 {
// Fallback для старых браузеров
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>