Обработка ошибок авторизации

В STOMP-протоколе авторизация тесно связана с этапом установления соединения и последующей подпиской на брокер сообщений. В STOMP.js контроль доступа реализуется через заголовки CONNECT, а также через серверную валидацию, которая может завершиться как отказом в соединении, так и асинхронной ошибкой после успешного подключения.

Ключевая особенность заключается в том, что ошибка авторизации не всегда проявляется в момент вызова connect. В зависимости от реализации брокера (Spring WebSocket, RabbitMQ STOMP plugin, ActiveMQ) отказ может быть возвращён:

  • в виде HTTP-ошибки при WebSocket handshake,
  • в виде STOMP ERROR frame,
  • через закрытие соединения с кодом причины,
  • после установления сессии при попытке подписки.

Передача авторизационных данных в STOMP.js

Авторизация в STOMP.js обычно реализуется через заголовки CONNECT:

client.connect(
  {
    Authorization: `Bearer ${token}`
  },
  onConnect,
  onError
);

или в более новых версиях API:

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

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


Основные типы ошибок авторизации

HTTP-ошибки WebSocket handshake

При использовании STOMP поверх WebSocket сервер может отклонить соединение ещё до установления канала связи. В этом случае STOMP.js не получает STOMP frame, а срабатывает обработчик WebSocket ошибки.

Типичные сценарии:

  • 401 Unauthorized — отсутствует или недействителен токен
  • 403 Forbidden — доступ запрещён для данного пользователя
  • 302 Redirect — попытка перенаправления на страницу логина

Обработка:

client.onWebSocketEr ror = (event) => {
  console.error("WebSocket error:", event);
};

client.onStompEr ror = (frame) => {
  console.error("STOMP error:", frame.headers["message"]);
};

Важно учитывать, что onStompError не будет вызван, если соединение не дошло до STOMP-уровня.


STOMP ERROR frame

После установления соединения сервер может отправить специальный ERROR frame:

ERROR
message: Authentication failed
content-type: text/plain

Access denied

STOMP.js обрабатывает это через onStompError:

const client = new Client({
  onStompError: (frame) => {
    console.error("Broker error:", frame.headers["message"]);
    console.error("Details:", frame.body);
  }
});

В этой модели соединение считается установленным, но сервер завершает сессию принудительно.


Закрытие соединения брокером

Некоторые брокеры не отправляют ERROR frame, а сразу закрывают соединение. В этом случае возникает событие WebSocket close.

client.onWebSocketCl ose = (event) => {
  console.warn("Connection closed:", event.code, event.reason);
};

Коды закрытия:

  • 1008 — policy violation (часто используется при auth fail)
  • 1011 — internal error на сервере
  • кастомные коды брокера

Ошибки подписки (SUBSCRIBE authorization)

Даже при успешном CONNECT авторизация может проверяться на уровне destination.

Пример: доступ к /topic/admin разрешён только определённой роли.

В этом случае:

  • CONNECT проходит успешно
  • SUBSCRIBE отклоняется ERROR frame или отсутствием сообщений

Пример обработки:

client.subscribe("/topic/admin", (message) => {
  console.log(message.body);
});

Если брокер возвращает ошибку, она может прийти как отдельный ERROR frame или как системное сообщение в error destination.


Центральная модель обработки ошибок STOMP.js

Корректная обработка авторизации строится на трёх уровнях:

1. WebSocket уровень

Отвечает за транспортное соединение.

onWebSocketError
onWebSocketClose

Основная задача — фиксация проблем handshake и сетевых сбоев.


2. STOMP уровень

Отвечает за протокол и брокер.

onStompError

Обрабатывает:

  • отказ в CONNECT
  • ошибки брокера
  • нарушения протокола

3. Прикладной уровень

Отвечает за бизнес-логику доступа.

Сюда относятся:

  • отказ в подписке
  • ошибки авторизации по destination
  • истечение токена во время сессии

Типовые сценарии отказа авторизации

Истечение JWT токена

При долгоживущих соединениях токен может устаревать. Сервер при этом:

  • разрывает соединение,
  • отправляет ERROR frame,
  • или блокирует дальнейшие операции.

Практика обработки:

onStompError: (frame) => {
  if (frame.headers["message"] === "Token expired") {
    refreshToken();
  }
}

Неверный токен при CONNECT

Если токен недействителен, сервер часто завершает соединение сразу.

Характер поведения:

  • отсутствует STOMP session
  • срабатывает WebSocket close
  • возможен 401 на handshake

Недостаточные права доступа

Сценарий RBAC:

  • пользователь аутентифицирован
  • но не имеет роли для конкретного destination

Поведение:

  • CONNECT успешен
  • SUBSCRIBE отклоняется

Стратегии централизованной обработки ошибок

Единый обработчик состояния соединения

const state = {
  connected: false,
  lastError: null
};

client.onConn ect = () => {
  state.connected = true;
};

client.onStompEr ror = (frame) => {
  state.lastError = frame;
  state.connected = false;
};

client.onWebSocketCl ose = () => {
  state.connected = false;
};

Дифференциация ошибок по кодам и заголовкам

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

  • message
  • version
  • content-type
  • кастомные server headers

Пример анализа:

onStompError: (frame) => {
  const msg = frame.headers["message"];

  if (msg.includes("unauthorized")) {
    handleUnauthorized();
  }

  if (msg.includes("forbidden")) {
    handleForbidden();
  }
};

Обработка через interceptor connectHeaders

Перед каждым reconnect возможно обновление токена:

client.beforeConnect = () => {
  client.connectHeaders.Authorization = `Bearer ${getToken()}`;
};

Это снижает вероятность повторных auth ошибок после реконнекта.


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

STOMP.js поддерживает автоматический reconnect, однако при ошибках авторизации он может приводить к циклу повторных неудачных подключений.

Типичная проблема:

  • токен истёк
  • reconnect выполняется с тем же токеном
  • сервер снова отклоняет CONNECT

Решение заключается в разделении типов ошибок:

  • сетевые ошибки → reconnect
  • auth ошибки → stop reconnect + refresh token
onStompError: (frame) => {
  const msg = frame.headers["message"];

  if (msg.includes("unauthorized")) {
    client.deactivate();
    refreshToken();
  }
};

Различия поведения брокеров

Spring WebSocket STOMP

  • строгая интеграция с SecurityContext
  • часто используется 401/403 на handshake
  • ERROR frame содержит подробный message

RabbitMQ STOMP plugin

  • более простая модель
  • часто закрывает соединение без детального ERROR frame

ActiveMQ

  • поддерживает детализированные STOMP error frames
  • может возвращать broker-specific headers

Практическая модель устойчивой авторизации

Устойчивость к ошибкам авторизации достигается сочетанием:

  • проверки токена перед подключением
  • обновления токена перед reconnect
  • обработки ERROR frame
  • контроля WebSocket close событий
  • разделения транспортных и прикладных ошибок

Ключевой принцип заключается в том, что STOMP.js не предоставляет единого механизма auth error handling, и вся логика распределяется по слоям протокола и транспортного соединения.