Работа с защищенными каналами

Подключение к защищённым STOMP-каналам начинается с выбора транспорта, который обеспечивает шифрование на уровне канала связи. В большинстве современных приложений используется WebSocket поверх TLS, то есть схема wss://, где весь трафик между клиентом и сервером проходит через зашифрованное соединение.

При работе с STOMP.js защищённый канал формируется не только на уровне транспорта, но и на уровне протокола обмена сообщениями. STOMP добавляет поверх WebSocket семантику команд (CONNECT, SUBSCRIBE, SEND), что позволяет встроить механизмы авторизации и контроля доступа непосредственно в момент установления соединения.

Базовый принцип заключается в замене небезопасного ws:// на wss://. Это аналог HTTPS для WebSocket.

import { Client } from "@stomp/stompjs";

const client = new Client({
  brokerURL: "wss://api.example.com/ws",
  reconnectDelay: 5000,
});

Использование TLS гарантирует:

  • шифрование всех STOMP-сообщений
  • защиту от перехвата токенов и payload
  • целостность передаваемых данных
  • проверку подлинности сервера через сертификат

Важно учитывать, что защита канала не заменяет прикладную авторизацию: даже при wss сервер обязан проверять права доступа на уровне STOMP-команд.

Передача токенов в момент CONNECT

Наиболее распространённый способ авторизации — передача токена в заголовках STOMP CONNECT. В STOMP.js это реализуется через поле connectHeaders.

const token = localStorage.getItem("access_token");

const client = new Client({
  brokerURL: "wss://api.example.com/ws",
  connectHeaders: {
    Authorization: `Bearer ${token}`,
  },
  reconnectDelay: 5000,
});

На серверной стороне этот заголовок извлекается при обработке CONNECT и используется для валидации пользователя до завершения установления сессии.

Ключевой момент заключается в том, что токен передаётся один раз — в момент handshake STOMP, а не в каждом сообщении. Это снижает накладные расходы и уменьшает поверхность атаки.

Защита handshake WebSocket

Перед тем как STOMP-сессия вообще начнётся, происходит HTTP Upgrade-запрос. На этом этапе можно внедрить дополнительные механизмы защиты:

  • проверка cookies (session-based auth)
  • проверка Origin заголовка
  • валидация JWT в query string (менее предпочтительно)
  • блокировка неизвестных источников

Пример конфигурации серверной проверки (логика абстрактна):

function verifyHandshake(request) {
  const origin = request.headers.origin;
  const token = request.url.searchParams.get("token");

  if (!isAllowedOrigin(origin)) return false;
  if (!verifyJWT(token)) return false;

  return true;
}

Хотя STOMP ещё не активен на этом этапе, ошибки здесь полностью предотвращают установление канала.

Разделение прав доступа на уровне подписок

После установления соединения критически важно ограничивать доступ не только к подключению, но и к конкретным топикам. В STOMP это реализуется через серверную проверку SUBSCRIBE.

Пример логики:

  • пользователь A может подписаться на /user/queue/messages
  • пользователь B не имеет доступа к /admin/*

Даже если клиент вручную попытается подписаться на запрещённый канал, сервер обязан отклонить подписку.

Работа с пользовательскими очередями

В защищённых системах часто используется модель user-destination, где сообщения адресуются конкретному пользователю:

client.subscribe("/user/queue/notifications", (message) => {
  console.log(JSON.parse(message.body));
});

Сервер сопоставляет STOMP-сессию с идентичностью пользователя, полученной из токена. Это позволяет исключить утечку сообщений между пользователями даже при одинаковых подписках.

Heartbeat и защита от обрыва сессии

Защищённые каналы требуют контроля живости соединения. STOMP.js поддерживает heartbeat-механизм:

const client = new Client({
  brokerURL: "wss://api.example.com/ws",
  heartbeatIncoming: 10000,
  heartbeatOutgoing: 10000,
});

Heartbeat выполняет две функции:

  • обнаружение разрыва соединения
  • защита от idle timeout на прокси и балансировщиках

На защищённых каналах это также снижает риск удержания «зависших» сессий, которые могут быть использованы повторно.

Обновление токена без разрыва соединения

В долгоживущих соединениях токен может устаревать. В STOMP нет встроенного механизма refresh, поэтому используется прикладная стратегия:

  • отслеживание истечения JWT
  • принудительное переподключение клиента
  • передача нового токена в connectHeaders
function reconnectWithNewToken(newToken) {
  client.deactivate();

  client.connectHeaders = {
    Authorization: `Bearer ${newToken}`,
  };

  client.activate();
}

В защищённых системах важно, чтобы сервер инвалидировал старые сессии при смене токена, иначе возможен доступ по устаревшему контексту.

Использование SockJS как защищённого fallback-канала

При ограничениях сети WebSocket может быть недоступен, и используется SockJS как транспорт-эмуляция. При этом защита сохраняется, так как поверх всё равно используется тот же STOMP-уровень.

import SockJS from "sockjs-client";

const client = new Client({
  webSocketFactory: () => new SockJS("https://api.example.com/ws"),
});

Здесь критично, что даже при HTTP-fallback канал должен оставаться защищённым через HTTPS, иначе токены могут быть перехвачены.

Политики серверной валидации сообщений

Защита канала не ограничивается соединением и подписками. Каждое сообщение SEND также должно проходить проверку:

  • проверка прав отправителя
  • проверка целевого destination
  • фильтрация payload
  • ограничение размера сообщения

Особенно важно это в multi-tenant системах, где разные клиенты используют один брокер.

Изоляция сессий и предотвращение hijacking

STOMP-сессия должна быть строго привязана к:

  • userId из токена
  • WebSocket session id
  • серверному контексту безопасности

При любой несостыковке сессия немедленно разрывается. Это предотвращает:

  • захват сессии через повторное подключение
  • подмену пользователя при повторном handshake
  • повторное использование старых subscription id

Работа через reverse proxy и балансировщики

В защищённых архитектурах WebSocket/STOMP почти всегда проходит через прокси (Nginx, Traefik, API Gateway). Важно корректно настроить:

  • поддержку Upgrade headers
  • увеличение timeout idle connections
  • проброс Authorization headers
  • отключение буферизации

Ошибка на уровне прокси часто приводит к «ложной защищённости», когда соединение формально устанавливается, но сообщения теряются или авторизация не проходит.

Контроль доступа на уровне брокера

Если используется брокер (например, RabbitMQ или встроенный брокер Spring), он может дополнительно проверять права:

  • разрешённые destination patterns
  • пользовательские роли
  • ACL на очереди

Это создаёт второй слой защиты поверх STOMP-логики приложения и снижает риск компрометации при ошибке в клиенте.