Теоретический конспект: Организация интерфейса с пользователем — контроль

Дисциплина «ОП.08 Основы проектирования баз данных» | Тема 4. Проектирование структур баз данных (§2.2 с.7–8) | СИЭУиП

1. Архитектура эшелонированного контроля и блок-схема потока данных

В современных информационных системах пользовательский интерфейс отделен от СУБД сетевым уровнем. Любые данные, вводимые человеком через формы на веб-странице или в мобильном приложении, проходят многоступенчатую цепочку проверок. В промышленной архитектуре реализуется принцип «Defense in Depth» (эшелонированная оборона):

+---------------------------------------------------------------------------------------------------+ | БЛОК-СХЕМА ПОТОКА ВАЛИДАЦИИ ДАННЫХ | +---------------------------------------------------------------------------------------------------+ [ ПОЛЬЗОВАТЕЛЬ ] | Ввод символов в форму (инпуты, селекторы, чекбоксы) v +-----------------------------+ | 1. КЛИЕНТСКИЙ КОНТРОЛЬ (UX) | ---> Ошибка? ---> Мгновенная красная рамка, текст подсказки, | - Маски ввода и regex | блокировка кнопки Submit (is-invalid) | - Проверка контрольных сумм| | - Временные интервалы дат | +-----------------------------+ | Валидно (OK) -> Отправка HTTP POST / JSON Payload v +-----------------------------+ | 2. API SERVER (БЭКЕНД) | ---> Ошибка схемы? -> Ответ 400 Bad Request (JSON с ошибками) | - Санитизация строк | | - Аутентификация / Сессия | +-----------------------------+ | Формирование Prepared Statement с плейсхолдерами ($1, $2, ...) v +-----------------------------+ | 3. ПАРАМЕТРИЗОВАННЫЙ ЗАПРОС | ---> Защита от SQL Injection: СУБД скомпилировала AST, | - Разделение кода и данных | байты пользователя воспринимаются СТРОГО как литералы! +-----------------------------+ | Передача параметров по бинарному протоколу в СУБД v +-----------------------------+ | 4. СУБД: ДЕКЛАРАТИВНЫЙ И | | ТРАНЗАКЦИОННЫЙ КОНТРОЛЬ | | - NOT NULL, CHECK, UNIQUE | | - EXCLUDE USING gist | ---> Конфликт? -> ВЫБРОС ИСКЛЮЧЕНИЯ СУБД: | - BEFORE-триггеры (остатки)| Коды SQLSTATE (23505, 23514, 23P01, P0001) | - SELECT ... FOR UPDATE | Автоматический ROLLBACK транзакции! +-----------------------------+ | Ошибка перехвачена драйвером бэкенда v +-----------------------------+ | 5. ERROR MAPPING (ТРАНСЛЯЦИЯ| ---> Преобразование технического кода SQLSTATE | ТЕХНИЧЕСКИХ ОШИБОК) | в понятный человеку текст (User-Friendly UI Notification) +-----------------------------+ БЕЗ раскрытия имен таблиц и схемы базы данных! | v [ ЭКРАН ПОЛЬЗОВАТЕЛЯ: «Клиент с данным ИНН уже зарегистрирован» / «Номер занят на эти даты» ]
Ключевая аксиома проектирования: Клиентская валидация существует для удобства пользователя (UX) и снижения сетевой нагрузки. Серверная валидация и ограничения СУБД существуют для сохранения целостности и безопасности данных. Никогда не доверяйте данным, пришедшим с клиента!

2. Сравнительная характеристика клиентского и серверного контроля

Распределение ответственности между уровнями архитектуры представлено в сравнительной таблице:

Характеристика Клиентский контроль (Frontend) Серверный контроль (Backend / СУБД)
Среда выполнения Браузер пользователя (V8, JavaScript / TypeScript, HTML5 Constraint API). Сервер приложений (Node.js, Python, Java, Go) и ядро реляционной СУБД (PostgreSQL).
Главная цель Мгновенный отклик интерфейса, снижение холостого трафика, подсказка правильного формата. Гарантия целостности данных (ACID), исключение инъекций, обработка конкурентных транзакций.
Скорость отклика 0–5 миллисекунд (выполняется локально в оперативной памяти клиентского устройства). 15–150 миллисекунд (задержка сети, чтение блокировок, обращение к дисковым индексам).
Степень доверия Нулевая. Код клиента может быть отключен, переписан через консоль разработчика или обойден запросом через curl. Абсолютная. Ядро СУБД физически отменяет транзакцию и не допускает некорректную запись на диск.
Инструменты реализации HTML5-атрибуты (required, pattern), события input, blur, маски полей, RegExp в JS. Ограничения CHECK, UNIQUE, FOREIGN KEY, триггеры BEFORE INSERT/UPDATE, транзакции SELECT FOR UPDATE.
Типичные сценарии Проверка формата email, проверка длины и контрольной суммы ИНН, проверка Дата_выезда > Дата_заезда. Проверка уникальности ИНН среди миллионов клиентов, проверка остатка на счете, блокировка слота времени.

3. Правила составления регулярных выражений для валидации экранных форм

Регулярные выражения (Regular Expressions, RegExp) являются стандартом де-факто для проверки формата текстовых данных в формах ввода.

3.1. Фундаментальное правило: граничные якоря ^ и $

Самая распространенная ошибка разработчиков — отсутствие якорей начала строки (^) и конца строки ($). Без якорей регулярное выражение ищет любое совпадение в подстроке, что делает валидацию фиктивной:

// НЕПРАВИЛЬНО (без якорей):
const badRegex = /\d{10}/;
badRegex.test("Мой ИНН 1234567890 и еще мусор"); // Вернет TRUE! (пропустит грязь в БД)

// ПРАВИЛЬНО (строго от начала до конца):
const goodRegex = /^\d{10}$/;
goodRegex.test("1234567890");                   // TRUE (ровно 10 цифр)
goodRegex.test("Мой ИНН 1234567890");          // FALSE (отклонено)

3.2. Стандартные шаблоны валидации реквизитов

Поле экранной формы Спецификация формата Регулярное выражение (RegExp)
ИНН юридического лица Строго 10 арабских цифр ^\d{10}$
ИНН индивидуального предпринимателя Строго 12 арабских цифр ^\d{12}$
ОГРН юридического лица Строго 13 арабских цифр ^\d{13}$
БИК Банка РФ Строго 9 арабских цифр (начинается с 04) ^04\d{7}$
Банковский счет РФ (расчетный/корр.) Строго 20 цифр (начинается с 407, 408, 301 и др.) ^\d{20}$
Мобильный телефон РФ Формат с кодом страны +7 и 10 цифрами номера ^\+7\d{10}$ или с маской ^\+7 \(\d{3}\) \d{3}-\d{2}-\d{2}$
Адрес электронной почты (Email) Имя пользователя, символ @, домен и зона от 2 букв ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$

3.3. Клиентская проверка контрольных сумм (Алгоритм ИНН-10)

Одной лишь проверки регулярным выражением ^\d{10}$ недостаточно, так как 10-й знак ИНН юридического лица является контрольным разрядом, вычисляемым по специальным весовым коэффициентам:

// Алгоритм проверки контрольного разряда ИНН 10 знаков:
function validateInn10(inn) {
    if (!/^\d{10}$/.test(inn)) return false;
    const weights = [2, 4, 10, 3, 5, 9, 4, 6, 8];
    let sum = 0;
    for (let i = 0; i < 9; i++) {
        sum += parseInt(inn[i], 10) * weights[i];
    }
    const checkDigit = (sum % 11) % 10;
    return checkDigit === parseInt(inn[9], 10);
}

4. Защита от SQL-инъекций: параметризованные запросы (Prepared Statements)

SQL-инъекция возникает в том случае, когда непроверенный ввод пользователя напрямую конкатенируется в тело SQL-запроса, в результате чего интерпретатор СУБД воспринимает данные как управляющие команды.

Классическая атака тавтологии:
Если запрос сформирован строкой "SELECT * FROM accounts WHERE login = '" + login + "'", злоумышленник вводит admin' OR '1'='1.
Итоговый запрос превращается в SELECT * FROM accounts WHERE login = 'admin' OR '1'='1', что приводит к обходу аутентификации и компрометации системы.

Почему параметризация дает 100% защиту?
Параметризованный запрос разделяет выполнение на два независимых этапа:

  1. Компиляция (PREPARE): СУБД компилирует запрос с маркерами параметров (плейсхолдерами $1, $2) в бинарное дерево синтаксического разбора (AST — Abstract Syntax Tree). Структура запроса жестко фиксируется в памяти.
  2. Исполнение (EXECUTE): Значения параметров передаются отдельно по протоколу передачи данных. Интерпретатор СУБД обрабатывает их строго как константные литералы соответствующего типа (строка, число, дата). Даже если параметр содержит символы ' ; -- DROP TABLE, они не могут выйти за рамки строкового значения и не могут быть выполнены как код!
-- Как параметризация выглядит на уровне протокола PostgreSQL:
-- Шаг 1: Подготовка запроса с типами параметров
PREPARE find_user (text, text) AS
    SELECT id, email, full_name FROM users WHERE login = $1 AND password_hash = $2;

-- Шаг 2: Безопасное выполнение с любыми специальными символами
EXECUTE find_user('admin\' OR 1=1 --', 'secret_hash_value');
-- Запрос будет искать пользователя с буквальным логином "admin' OR 1=1 --" и не вернет лишних данных!

5. Серверный контроль: триггеры BEFORE и изоляция транзакций

Некоторые бизнес-правила невозможно выразить статическим предикатом CHECK (например, проверка наличия товара на складе перед созданием заказа или списание баланса с лицевого счета). Для таких задач в СУБД применяются серверные триггеры BEFORE INSERT OR UPDATE и транзакционные блокировки.

5.1. Пример серверного триггера проверки остатка товара (PostgreSQL)

CREATE OR REPLACE FUNCTION check_stock_availability()
RETURNS TRIGGER AS $$
DECLARE
    current_stock INT;
BEGIN
    -- Блокируем строку товара для предотвращения параллельного изменения остатка
    SELECT stock_quantity INTO current_stock
    FROM warehouse_products
    WHERE product_id = NEW.product_id
    FOR UPDATE;

    IF current_stock < NEW.quantity THEN
        RAISE EXCEPTION 'Недостаточно товара на складе: запрошено %, в наличии %',
            NEW.quantity, current_stock
            USING ERRCODE = 'check_violation';
    END IF;

    -- Автоматически уменьшаем складской остаток
    UPDATE warehouse_products
    SET stock_quantity = stock_quantity - NEW.quantity
    WHERE product_id = NEW.product_id;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_check_stock
BEFORE INSERT ON order_items
FOR EACH ROW
EXECUTE FUNCTION check_stock_availability();

5.2. Проблема Race Condition и почему SELECT перед INSERT не работает

Начинающие разработчики часто пишут в коде бэкенда:

// Антипаттерн "Проверка перед вставкой":
if (db.query("SELECT COUNT(*) FROM bookings WHERE room_id = 1 AND date = '2026-10-15'") == 0) {
    db.query("INSERT INTO bookings (room_id, date) VALUES (1, '2026-10-15')");
}

Если два пользователя нажмут кнопку одновременно, оба параллельных потока выполнят SELECT, оба получат результат 0, после чего оба выполнят INSERT. В результате в базе образуется овербукинг (двойное бронирование)!
Решение на уровне СУБД: создание уникального индекса UNIQUE (room_id, booking_date) или ограничения исключения EXCLUDE USING gist (room_id WITH =, booking_range WITH &&). В этом случае СУБД аппаратно заблокирует вторую транзакцию и вернет ошибку 23505 или 23P01.

6. Обработка ошибок СУБД и трансляция в пользовательский интерфейс (Error Mapping)

В случае отказа транзакции на стороне СУБД сервер приложений обязан перехватить исключение, извлечь код SQLSTATE и сопоставить его со словарем дружественных экранных сообщений.

+-----------------------------------------------------------------------------------------------+ | ТАБЛИЦА СООТВЕТСТВИЯ (ERROR MAPPING DICTIONARY) | +-----------------------------------------------------------------------------------------------+ | SQLSTATE | Системный конфликт СУБД | Понятное сообщение пользователю | +----------+--------------------------------+---------------------------------------------------+ | 23505 | unique_violation | «Запись с такими реквизитами уже зарегистрирована»| | 23514 | check_violation | «Значение выходит за рамки допустимого диапазона» | | 23503 | foreign_key_violation | «Связанная запись не найдена в справочнике» | | 23P01 | exclusion_violation (gist) | «Выбранный ресурс уже забронирован на эти даты» | | 40001 | serialization_failure | «Операция прервана из-за высокой нагрузки. | | | | Пожалуйста, повторите попытку» | | P0001 | raise_exception (триггер) | Динамический текст ошибки триггера бизнес-логики | +-----------------------------------------------------------------------------------------------+
Золотое правило интерфейса: Пользователь никогда не должен видеть системные термины базы данных: слова SQLSTATE, constraint, table, null pointer или стектрейсы. Сообщение должно четко объяснять:
  1. Что именно пошло не так (на человеческом языке).
  2. Какое конкретное действие нужно совершить пользователю для исправления ситуации (изменить дату, проверить ИНН, пополнить счет).

7. Вопросы для самопроверки и закрепления

Вопрос 1: Достаточность клиентской валидации

Если на веб-форме написан совершенный JavaScript-валидатор, проверяющий формат и контрольную сумму всех полей, можно ли отказаться от ограничений CHECK и NOT NULL в схеме базы данных?

Вопрос 2: Механизм защиты Prepared Statements

Почему параметризованный запрос делает невозможной SQL-инъекцию, даже если входная строка пользователя содержит кавычки и опасные инструкции ; DROP TABLE users; --?

Вопрос 3: Разница между регулярным выражением с якорями и без

Какую уязвимость порождает регулярное выражение /\d{10}/ по сравнению с /^\d{10}$/ при проверке ИНН?

Вопрос 4: Перехват кодов SQLSTATE

Какую опасность несет вывод сырого сообщения ERROR: duplicate key value violates unique constraint "clients_inn_key" прямо на экран пользователя и как его правильно обработать?