Генерация идентификаторов на фронтенде: архитектурные риски и альтернативные подходы

Описанные в статье диалоги и ситуации приведены исключительно для иллюстрации архитектурных проблем. Любое сходство с реальными разработчиками, командами или рабочими процесссами являются случайностью.

Представьте ситуацию. Вы — фронтенд-разработчик. Обсуждаете с бэкенд-коллегой, как генерировать ID для новых объектов. Предлагаете сделать на бэкенде простой метод, который будет выдавать ID порциями. Это стандартная практика, так делают все, кто заботится о безопасности и масштабируемости.

А в ответ слышите:

«Зачем? Фронт может генерировать сам. UUID v4 — это же просто. Коллизий практически не будет. А валидацию мы на бэкенде сделаем, не переживай».

Знакомая ситуация?

Если да — эта статья для вас.

А если нет — возможно, она убережет вас от будущих проблем и даст готовые аргументы для следующего спора.

Спойлер: генерация ID на фронтенде — это антипаттерн. И сейчас я объясню почему.

Часть 1. Почему вообще возникает этот спор?

На первый взгляд логика бэкенда кажется разумной:

«Зачем делать лишний запрос к серверу, если ID можно сгенерировать прямо в браузере? UUID v4 генерируется за микросекунды. Вероятность коллизии — 1 на 10^38, это меньше, чем вероятность сбоя жесткого диска. Идеальное решение!»

Звучит убедительно. Но только на первый взгляд.

Проблема в фундаментальном допущении: код на фронтенде выполняется в среде, которую полностью контролирует пользователь. Браузер — это не доверенная среда.

Никогда. Любой код, выполняющийся на клиенте, может быть изменен, подменен или скомпрометирован.

И пока вы не примете это как аксиому, любые рассуждения о «безопасной генерации на фронте» будут бессмысленны.

Часть 2. 6 главных причин, почему это плохо

Причина №1. Клиентский код можно изменить за 5 секунд

Это самый главный, фундаментальный аргумент. Пользователь — полный хозяин своего браузера. Он может открыть консоль разработчика и сделать буквально что угодно.

// Ваша "честная" функция генерации function generateId() { return crypto.randomUUID(); } // То, что делает пользователь в консоли за 5 секунд window.generateId = function() { return 'HACKED-1'; // Любой ID, который захочет } // Теперь все вызовы вашей функции будут возвращать подставной ID const newId = generateId(); // 'HACKED-1'

Пользователь может:

— Подменить любую JavaScript-функцию в рантайме;

— Перехватить и изменить любой сетевой запрос (через прокси или devtools);

— Использовать расширения браузера для модификации поведения страницы;

— Написать свой скрипт и отправить на сервер что угодно.

Вывод: нельзя доверять данным, которые приходят с клиента. Никогда. Даже если вы их сами сгенерировали.

Причина №2. Нет гарантии уникальности

«Вероятность коллизии UUID v4 ничтожно мала» — да, это правда

— Маловероятно ≠ невозможно. Теоретически коллизия может произойти в любой момент;

— При высокой нагрузке вероятность растет. Чем больше ID сгенерировано, тем выше шанс;

— Разные пользователи могут сгенерировать одинаковый ID. Два запроса с одним ID → один пользователь получит ошибку;

— Плохая энтропия. Не все генераторы случайных чисел одинаково хороши.

// Пользователь A и пользователь B одновременно генерируют ID // Оба получили один и тот же ID (коллизия!) const userAid = crypto.randomUUID(); // 'abc-123-def' const userBid = crypto.randomUUID(); // 'abc-123-def' (такой же!) // Оба отправляют запросы на бэкенд await fetch('/api/orders', { body: JSON.stringify({ id: userAid }) }); await fetch('/api/orders', { body: JSON.stringify({ id: userBid }) }); // Первый запрос создает запись в БД // Второй падает с ошибкой "duplicate key" // Пользователь B видит ошибку, хотя не сделал ничего плохого

Реальная история из практики: один сервис генерировал ID через `Date.now()`.

В один момент два запроса пришли в одну миллисекунду — оба получили одинаковый ID. База данных упала на constraint violation. Проблему «починили» добавлением `Math.random()`, но коллизии продолжились при высокой нагрузке.

Перешли на бэкенд-генерацию — проблема ушла навсегда.

Причина №3. Уязвимость для IDOR-атак

IDOR - Insecure Direct Object — одна из самых распространенных и опасных уязвимостей веб-приложений. Она регулярно попадает в OWASP Top 10.

// Фронт отправляет запрос на создание заказа const newOrder = { id: crypto.randomUUID(), // '550e8400-e29b-41d4-a716-446655440000' userId: 123, amount: 1000 }; await fetch('/api/orders', { method: 'POST', body: JSON.stringify(newOrder) }); // Злоумышленник легко может подменить ID прямо в консоли const hackedOrder = { id: '550e8400-e29b-41d4-a716-446655440001', // Просто соседний ID userId: 456, // Чужой пользователь amount: 1000 }; await fetch('/api/orders', { method: 'POST', body: JSON.stringify(hackedOrder) }); // Если бэкенд доверяет фронтенду — данные запишутся с чужим ID

Даже если бэкенд проверяет права доступа (а он, конечно, должен это делать), сама возможность манипулировать ID — это лишняя поверхность для атаки.

Чем меньше входных данных доверяет бэкенд — тем безопаснее система.

Причина №4. Невозможно защититься от флуда

Когда ID генерируются на фронте, злоумышленник может организовать простую, но эффективную атаку:

// Всего несколько строк кода в консоли браузера for (let i = 0; i < 1000000; i++) { fetch('/api/orders/create', { method: 'POST', body: JSON.stringify({ id: crypto.randomUUID(), // Поддельный ID data: 'spam' }) }); }

Последствия:

— Ваша база данных забивается мусорными записями;

— Реальные пользователи получают ошибки или тормоза;

— Растут расходы на хранение и вычислительные ресурсы;

— Бэкенд не может отличить «честную» генерацию от вредоносной.

Rate limiting по IP? Злоумышленник использует ботнет. Капча? Это только усложнит жизнь обычным пользователям. Единственный надежный способ — контролировать выдачу ID на бэкенде.

Причина №5. Нет аудита и централизованного контроля

Когда ID рождается на клиенте, бэкенд теряет критически важную информацию:

// Бэкенд получает запрос на создание объекта app.post('/api/orders', (req, res) => { const { id, userId, amount } = req.body; // Бэкенд НЕ ЗНАЕТ: // - Кто реально создал этот ID (может быть подделка) // - В какое точное время (timestamp с клиента нельзя доверять) // - С какого устройства и IP-адреса (уже есть, но ID мог быть старым) // - Это был человек или автоматический скрипт db.insert({ id, userId, amount }); });

Без этой информации вы не сможете:

— Расследовать инциденты безопасности;

— Отследить необычную активность;

— Подготовить отчет для регулятора;

— Понять, кто на самом деле создал подозрительную запись.

В системах с требованиями к аудиту (финансы, медицина, персональные данные) генерация ID на фронте просто неприемлема.

Причина №6. Проблемы с масштабированием и B2B-сценариями

Сегодня у вас только веб-фронтенд. А завтра появится мобильное приложение, десктоп-клиент, API для партнеров.

// Web: crypto.randomUUID() // iOS: NSUUID().uuidString // Android: UUID.randomUUID().toString() // Node.js backend: require('crypto').randomUUID() // Python backend: import uuid; str(uuid.uuid4()) // Проблемы: // - Разные алгоритмы → разное качество случайности // - Разные баги в реализации // - Разные форматы (строчные/заглавные, с/без дефисов) // - Невозможно гарантировать уникальность МЕЖДУ разными типами клиентов

Правильная архитектура: бэкенд — единый источник правды для ID. Только так можно гарантировать уникальность и контролируемость при любом количестве и типе клиентов.

Часть 3. А что насчет валидации на бэкенде?

«Ну и пусть фронт генерирует, мы всё проверим на своей стороне!» — часто слышу я в спорах.

Давайте разберем, что может проверить бэкенд, а что — нет.

// Бэкенд-валидация app.post('/api/orders', (req, res) => { const { id, userId, amount } = req.body; // Что МОЖЕТ проверить: if (!isValidUUID(id)) { return res.status(400).json({ error: 'Invalid ID format' }); } const exists = db.find({ id }); if (exists) { return res.status(409).json({ error: 'ID already exists' }); } // Чего НЕ МОЖЕТ проверить: // - Был ли ID сгенерирован "честно" или подделан? // - Действительно ли этот ID создан именно этим пользователем? // - Не пытается ли пользователь зафлудить систему? db.insert({ id, userId, amount }); });
Генерация идентификаторов на фронтенде: архитектурные риски и альтернативные подходы

Валидация проверяет факты, но не может проверить намерения. Злоумышленник всегда может сгенерировать ID в любом формате — и бэкенд не сможет это предотвратить.

Валидация — это защита от случайных ошибок, а не от злонамеренных атак.

Часть 4. Еще 15 причин (для самых любопытных)

Шести причин обычно достаточно, чтобы убедить. Но бывают случаи, когда нужна тяжелая артиллерия.

Проблемы с данными и целостностью

7. Потеря контекста создания ID

Бэкенд не знает, когда и при каких обстоятельствах был создан ID. timestamp с клиента нельзя доверять — его легко подделать.

8. Проблема каскадного обновления

Если объекты ссылаются друг на друга, замена ID на бэкенде ломает все ссылки.

// Объект с родительской ссылкой const campaign = { nodes: [ { id: "temp-1", parentId: null, name: "Root" }, { id: "temp-2", parentId: "temp-1", name: "Child" } // Ссылается на temp-1 ] }; // Отправляем на бэкенд await saveCampaign(campaign); // Бэкенд решил заменить temp-1 на 1001, а temp-2 на 1002 // Результат после замены: { nodes: [ { id: 1001, parentId: null, name: "Root" }, { id: 1002, parentId: "temp-1", name: "Child" } // Ссылка сломалась! ] }

9. Нарушение идемпотентности

При повторной отправке того же запроса (например, из-за медленного интернета) возникает конфликт уникальности.

// Пользователь нажал "Сохранить" дважды из-за медленного интернета // Запрос 1 await fetch('/api/orders', { method: 'POST', body: JSON.stringify({ id: "uuid-123", amount: 1000 }) }); // Запрос 2 (автоматический повтор) await fetch('/api/orders', { method: 'POST', body: JSON.stringify({ id: "uuid-123", amount: 1000 }) }); // Результат: // Первый запрос: создан объект с ID = uuid-123 // Второй запрос: пытается создать объект с ID = uuid-123 // → Ошибка 409 Conflict // Пользователь видит ошибку, хотя всё нормально

Проблемы с производительностью

10. Проблема «горячих» индексов при строковых ID

UUID v4 вставляется в случайное место B-Tree индекса, вызывая постоянную перебалансировку. При 10 000+ вставок/сек это становится узким местом.

11. Проблема пагинации и сортировки

Сортировка по UUID не равна сортировке по времени создания.

// Сортировка по ID не даст хронологический порядок const sortedById = orders.sort((a, b) => a.id.localeCompare(b.id)); // Приходится добавлять отдельное поле created_at const sortedByDate = orders.sort((a, b) => a.createdAt - b.createdAt); // Это лишний индекс, лишнее место, лишние запросы

Проблемы с безопасностью (расширение)

12. Утечка информации о бизнес-метриках

Конкуренты могут анализировать темпы роста вашего бизнеса по «расходу» ID.

// Конкурент создает фейковые заказы и смотрит на ID const id1 = await createFakeOrder(); // '550e8400...000' const id2 = await createFakeOrder(); // '550e8400...001' // Разница = 1 → за один запрос // За час можно замерить интенсивность создания реальных заказов

13. Атака через истощение пула ID

Если ID имеют ограниченный диапазон (например, 6-значный номер заказа), злоумышленник может исчерпать все доступные ID.

// Если ID — это 32-битное число (диапазон 0..4,294,967,295) // Злоумышленник может: for (let i = 0; i < 4294967295; i++) { await createOrderWithId(i); } // Все возможные ID заняты → DoS

14. Проблема с GDPR и «правом на забвение»

При удалении данных пользователя сложно найти все связанные объекты.

// Пользователь требует удалить все свои данные // Если ID не содержат информации о владельце: await db.delete('orders', { userId: 123 }); // А если ID ссылается на другой объект? // Если ID генерировал бэкенд и включил userId в структуру ID: // ID = (timestamp << 22) | (userId << 12) | sequence // Можно извлечь userId из ID без дополнительных запросов!

Проблемы с отладкой и эксплуатацией

15. Сложность воспроизведения багов

«У меня ошибка с объектом abc-123» — без доступа к машине пользователя невозможно понять, что произошло.

16. Проблема миграции данных

Смена формата ID требует координации между всеми клиентами одновременно.

// Вы решили перейти с UUID на числовые ID // Старые клиенты (не обновленные) продолжают генерировать UUID // В БД — каша из UUID и чисел // Приходится поддерживать оба формата вечно

17. Проблема тестирования

E2E-тесты не могут предсказать, какой ID будет создан.

// E2E тест test('creates a new order', async () => { const response = await createOrder({ amount: 100 }); // Не работает — ID каждый раз разный expect(response.id).toBe('abc-123-def'); // Приходится так: expect(response.id).toBeDefined(); expect(typeof response.id).toBe('string'); // Но это менее надежно });

Юридические и комплаенс-риски

18. Аудит для регуляторов

ФНС, PCI DSS, HIPAA, GDPR требуют доказательств, кто и когда создал каждую запись.

// Регулятор спрашивает: // "Кто создал заказ с ID 12345 и в какое время?" // Если ID генерировал бэкенд: // - В логах есть запись: пользователь X, время Y, IP Z // - Можно предоставить отчет // Если ID генерировал фронт: // - Нет доказательств, что ID создал именно пользователь // - Пользователь может сказать: "Это не я" // - Регулятор: "Ваша система не обеспечивает non-repudiation" // - Штрафы, потеря лицензии

19. Требования к последовательности ID

Некоторые регуляторы требуют строго возрастающие номера счетов/заказов.

// Требование: номер счета должен строго увеличиваться // Счет 1: 1000 // Счет 2: 1001 // Счет 3: 1002 // Фронт с UUID: // Счет 1: 550e8400-... // Счет 2: 6ba7b810-... (меньше лексикографически) // Нарушение требований → штраф // Только централизованный генератор на бэкенде может гарантировать порядок

---

Часть 5. Как правильно?

Архитектурно верное решение выглядит так:

// ========== БЭКЕНД (Node.js/Express) ========== const express = require('express'); const app = express(); // Генератор ID (простая версия с Redis или PostgreSQL) let currentId = 1000; app.post('/api/generate-ids', (req, res) => { const { count = 1, scope = 'default' } = req.body; // Проверка авторизации if (!req.user) { return res.status(401).json({ error: 'Unauthorized' }); } // Rate limiting (защита от флуда) if (isRateLimited(req.user.id)) { return res.status(429).json({ error: 'Too many requests' }); } // Генерация пачки ID const ids = []; for (let i = 0; i < count; i++) { ids.push(currentId++); } // Аудит auditLog.create(req.user.id, ids); res.json({ ids }); }); // ========== ФРОНТЕНД ========== class IDManager { constructor() { this.idPool = []; this.POOL_SIZE = 50; } async refillPool() { const response = await fetch('/api/generate-ids', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ count: this.POOL_SIZE }) }); const { ids } = await response.json(); this.idPool.push(...ids); } async getNextId() { if (this.idPool.length === 0) { await this.refillPool(); } return this.idPool.shift(); } } // Использование const idManager = new IDManager(); async function createNewNode() { const realId = await idManager.getNextId(); const newNode = { id: realId, // Уже настоящий ID от бэкенда! name: "New Node", createdAt: Date.now() }; // Отправляем на сохранение — ID менять не нужно await saveCampaign(newNode); return newNode; }

Почему это правильно?

Генерация идентификаторов на фронтенде: архитектурные риски и альтернативные подходы

А как же производительность?

Один дополнительный запрос на пачку из 50-100 ID — это ничтожная нагрузка. Особенно по сравнению с рисками, которые вы получаете при генерации на фронте.

Часть 6. Готовые аргументы для дискуссии с бэкендом

Если ваш бэкенд-коллега все еще настаивает на своем, используйте эти фразы.

Если говорит: «Мы будем валидировать на своей стороне»

«Валидация проверяет формат и уникальность, но не может проверить "честность" генерации. Злоумышленник может генерировать ID в любом формате, и вы не сможете это предотвратить. Вы предлагаете доверять данным из ненадежного источника — это базовая ошибка безопасности.»

Если говорит: «Коллизии UUID маловероятны»

«Коллизии маловероятны, но не невозможны. При высокой нагрузке вероятность растет. Кроме того, проблема не только в коллизиях, но и в атаках, флуде, предсказуемости и отсутствии аудита. Почему вы хотите перенести ответственность за уникальность на клиента?»

Если говорит: «Не хочу делать лишний метод»

«Метод генерации ID — это 15 строк кода на бэкенде. Если мы не можем договориться о такой простой вещи, какие у нас будут отношения по сложным задачам? Это вопрос архитектурной принципиальности.»

Если говорит: «Это оптимизация, меньше запросов»

«Мы говорим о 1 дополнительном запросе на пачку из 50-100 ID. Это ничтожная нагрузка. Ценой микрооптимизации вы приносите в жертву безопасность и надежность. Это неправильный trade-off.»

Если говорит: «В Google/Amazon так делают»

«Нет, не делают. В Google ID генерируются на бэкенде. Snowflake, Twitter Snowflake, Instagram ID — все это бэкенд-генераторы. Покажите мне хоть один крупный сервис, который доверяет генерацию ID клиенту.»

Часть 7. Что делать, если бэкенд не сдается?

Вариант 1. Эскалация

Вынесите вопрос на архитектурное обсуждение с Tech Lead или Архитектором. Это не «мнение фронтенда», это базовая безопасность и архитектурная грамотность.

Вариант 2. Зафиксировать риск

Создайте документ с описанием рисков и получите подписи.

Технический риск: Генерация ID на фронтенде Описание риска: В системе принято решение о генерации ID на фронтенде с последующим сохранением в БД Потенциальные последствия: — Коллизии ID при высокой нагрузке; — IDOR-атаки через подмену ID; — Флуд БД некорректными ID; — Невозможность B2B-интеграции; — Отсутствие аудита создания ID. Вероятность: Высокая Влияние: Критическое Статус: Риск принят (по требованию бэкенда)

Вариант 3. Компромисс (костыль)

Если бэкенд категорически отказывается, предложите схему с временными ID:

// Фронт генерирует ID со специальным префиксом const tempId = `TEMP_${Date.now()}_${Math.random()}`; // Бэкенд при получении такого ID заменяет на свой app.post('/api/save', (req, res) => { const { id, data } = req.body; if (id.startsWith('TEMP_')) { const realId = generateRealId(); // Сохраняем с realId // Возвращаем маппинг { tempId: realId } } });

Но это всё равно костыль. Правильное решение — метод на бэкенде.

---

Часть 8. Когда генерация на фронте все-таки допустима?

Справедливости ради, есть сценарии, где фронтенд-генерация ID может быть оправдана.

1. Оптимистичный UI (временные ID)

// Локальный ID для мгновенного отображения без ожидания сервера const tempId = `temp-${Date.now()}-${Math.random()}`; // Сразу показываем пользователю addNodeToUI({ id: tempId, name: "New Node" }); // Асинхронно сохраняем const response = await saveToBackend(node); // После сохранения заменяем tempId на реальный ID из ответа updateNodeIdInUI(tempId, response.realId);

2. Оффлайн-режим

Приложение генерирует ID локально, а при синхронизации бэкенд заменяет их на свои.

3. Прототипы и pet-проекты

Когда скорость разработки важнее безопасности и масштабируемости.

4. Абсолютно изолированные системы

Без сетевого взаимодействия, без БД, без нескольких пользователей.

Но для production-систем с высокой нагрузкой и требованиями к безопасности — никогда.

Часть 9. Чеклист для принятия решения

Перед тем как принять решение о генерации ID, ответьте на эти вопросы:

[ ] В системе есть больше одного пользователя?;

[ ] В системе есть больше одного сервера/инстанса БД?;

[ ] Есть требования к безопасности?;

[ ] Нужен аудит действий пользователей?;

[ ] Есть требования регуляторов (ФНС, PCI DSS, HIPAA, GDPR)?;

[ ] Планируются B2B-интеграции?;

[ ] Есть мобильное приложение (не вебвью)?;

[ ] Возможна высокая нагрузка (>1000 RPS)?;

[ ] Нужна возможность отладки инцидентов?;

[ ] Планируется масштабирование в будущем?.

Если вы отметили хотя бы 3 пункта — генерация ID на фронтенде не для вас.

---

Заключение

Генерация ID на фронтенде — это антипаттерн. Он основан на ложном допущении о том, что клиентскому коду можно доверять. На самом деле браузер пользователя — это враждебная среда, и любые данные, приходящие с клиента, должны рассматриваться как потенциально опасные.

Ваша задача — мягко, но настойчиво объяснить эти риски и настоять на правильном архитектурном решении. Или как минимум зафиксировать, что решение было принято против ваших рекомендаций.

Помните: безопасность — это общая ответственность. И иногда приходится спорить с коллегами, чтобы защитить систему от будущих проблем.

Ссылка на оригинальную статью: