Заголовки авторизации

STOMP (Simple Text Oriented Messaging Protocol) использует текстовые кадры (frames), где каждый кадр состоит из команды, набора заголовков и тела сообщения. Заголовки в STOMP выполняют ключевую роль: они передают метаданные соединения, включая параметры аутентификации и авторизации.

На уровне протокола авторизация чаще всего реализуется через заголовки кадра CONNECT или STOMP, который отправляется клиентом при установлении соединения с брокером сообщений.

Структура кадра подключения:

CONNECT
login: user
passcode: password
accept-version:1.2
host: example

\0

В современных приложениях вместо логина и пароля всё чаще используется токен-based авторизация, где ключевой элемент переносится в пользовательские заголовки.


Передача заголовков в STOMP.js

STOMP.js предоставляет возможность передавать произвольные заголовки при подключении к брокеру. Это делается через объект headers, который передаётся в метод connect.

Пример базового подключения:

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

const client = new Client({
  brokerURL: 'ws://localhost:8080/ws',
  connectHeaders: {
    login: 'user',
    passcode: 'password'
  }
});

client.activate();

В этом случае STOMP.js формирует CONNECT frame с указанными заголовками и отправляет его на сервер.

При использовании SockJS транспортный уровень не меняет семантику заголовков STOMP — они по-прежнему передаются внутри STOMP frame, а не HTTP-заголовками WebSocket handshake.


Использование токенов вместо login/passcode

В современных архитектурах чаще применяется JWT или аналогичные токены доступа. Это связано с тем, что STOMP-соединение обычно интегрируется в уже существующую систему аутентификации REST API или OAuth2.

Типовой вариант передачи JWT:

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

const client = new Client({
  brokerURL: 'ws://localhost:8080/ws',
  connectHeaders: {
    Authorization: `Bearer ${token}`
  }
});

client.activate();

Серверная сторона должна быть настроена на извлечение заголовка Authorization из STOMP CONNECT frame и проверку токена до установления сессии.


Различие между WebSocket handshake и STOMP headers

Частая ошибка заключается в попытке передать авторизационные данные через WebSocket HTTP handshake. В стандартной браузерной реализации WebSocket невозможно модифицировать HTTP headers напрямую.

STOMP решает эту проблему, перенося уровень авторизации выше — в STOMP frame.

Сравнение:

  • WebSocket handshake: ограниченный набор HTTP-заголовков
  • STOMP CONNECT frame: полностью контролируемые заголовки приложения

Таким образом, именно STOMP headers становятся основным механизмом авторизации.


Кастомные заголовки авторизации

STOMP допускает передачу любых пользовательских заголовков. Это позволяет реализовывать гибкие схемы безопасности.

Пример кастомной схемы:

const client = new Client({
  brokerURL: 'ws://localhost:8080/ws',
  connectHeaders: {
    'X-Auth-Token': token,
    'X-Client-Type': 'web',
    'X-Device-Id': 'device-123'
  }
});

Такие заголовки часто используются для:

  • идентификации устройства
  • передачи CSRF-подобных токенов
  • привязки сессии к клиенту
  • реализации multi-tenant логики

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


Авторизация через Spring WebSocket STOMP

В экосистеме Spring Security STOMP заголовки проходят через ChannelInterceptor, который позволяет перехватывать CONNECT frame до установления сессии.

Пример серверной обработки:

@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
    StompHeaderAccessor accessor =
        MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);

    if (StompCommand.CONNECT.equals(accessor.getCommand())) {
        String authHeader = accessor.getFirstNativeHeader("Authorization");

        if (authHeader == null || !validate(authHeader)) {
            throw new AccessDeniedException("Invalid token");
        }
    }

    return message;
}

После успешной проверки пользователь может быть привязан к WebSocket-сессии через Principal.


Авторизация в RabbitMQ STOMP plugin

RabbitMQ использует STOMP plugin, который поддерживает базовую авторизацию через login и passcode, но также допускает кастомные механизмы через плагины.

Пример подключения:

const client = new Client({
  brokerURL: 'ws://localhost:15674/ws',
  connectHeaders: {
    login: 'guest',
    passcode: 'guest'
  }
});

Однако в production-средах такой подход считается небезопасным, и обычно заменяется на:

  • reverse proxy с JWT проверкой
  • TLS termination с дополнительной авторизацией
  • custom authentication plugin

Повторное подключение и сохранение заголовков

STOMP.js автоматически выполняет reconnect при потере соединения, однако важно понимать, что connectHeaders должны быть либо статичными, либо обновляться при каждом подключении.

Пример динамического обновления токена:

client.beforeConnect = () => {
  const newToken = refreshToken();

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

Без такого обновления возможна ситуация, когда клиент переподключается с устаревшим токеном, что приводит к отказу в авторизации.


Заголовки и жизненный цикл сессии

После успешного CONNECT брокер формирует сессию, связанную с набором данных:

  • идентификатор сессии
  • пользователь (Principal)
  • разрешённые destination
  • подписки (subscriptions)

Заголовки CONNECT не используются повторно в SUBSCRIBE или SEND, однако могут быть частично дублированы в отдельных сообщениях для трассировки или авторизации на уровне маршрута.

Пример заголовков при отправке сообщения:

client.publish({
  destination: '/app/chat',
  body: JSON.stringify({ text: 'hello' }),
  headers: {
    'X-Request-Id': 'abc-123'
  }
});

Эти заголовки не участвуют в авторизации, если сервер явно не реализует такую логику.


Безопасность передачи авторизационных данных

Передача токенов через STOMP headers требует строгого соблюдения следующих принципов:

  • использование только защищённого канала wss://
  • минимизация времени жизни токена
  • отсутствие хранения токенов в логах брокера
  • проверка токена на стороне сервера до маршрутизации сообщений

Особое внимание требуется при использовании промежуточных прокси (NGINX, API Gateway), которые могут модифицировать или логировать STOMP frames при некорректной конфигурации.


Интеграция с OAuth2 и внешними провайдерами

При использовании OAuth2 токен обычно передаётся в STOMP CONNECT как Bearer token. Сервер извлекает его и валидирует через introspection endpoint или локальную JWT-подпись.

Типовой сценарий:

  1. Клиент получает access_token через OAuth2 flow
  2. Передаёт token в STOMP CONNECT headers
  3. Сервер валидирует токен
  4. Создаётся authenticated WebSocket session

Пример клиента:

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

Ограничения и особенности реализации заголовков

Несмотря на гибкость STOMP headers, существует ряд ограничений:

  • браузер не позволяет модифицировать WebSocket HTTP headers
  • некоторые брокеры ограничивают размер STOMP frame
  • бинарные данные в headers не поддерживаются
  • часть заголовков может быть отфильтрована прокси

Поэтому авторизация должна проектироваться с учётом того, что STOMP headers — это текстовый канал ограниченной структуры.


Поведение при ошибке авторизации

Если сервер отклоняет CONNECT frame, клиент получает ERROR frame:

ERROR
message:Authentication failed

Token invalid or expired
\0

STOMP.js в этом случае вызывает обработчик onStompError:

client.onStompEr ror = (frame) => {
  console.error(frame.headers['message']);
  console.error(frame.body);
};

После ошибки соединение считается не установленным, и дальнейшие подписки невозможны до повторного подключения с корректными заголовками.