В STOMP-протоколе аутентификация и авторизация не являются частью базовой спецификации WebSocket или самого STOMP-фреймворка, но почти всегда реализуются поверх него. Токен доступа становится основным механизмом идентификации клиента при установлении STOMP-соединения и дальнейшей маршрутизации сообщений на стороне брокера.
В контексте STOMP.js токен используется на этапе CONNECT
и далее может участвовать в обновлении соединения, переподключении и
контроле доступа к подпискам и отправке сообщений.
STOMP.js передаёт данные аутентификации через заголовки STOMP-фрейма
CONNECT. В отличие от HTTP-запросов, WebSocket не всегда
позволяет модифицировать HTTP headers в браузере, поэтому STOMP-уровень
становится основным местом передачи токена.
Пример передачи JWT:
import { Client } from "@stomp/stompjs";
const token = localStorage.getItem("access_token");
const client = new Client({
brokerURL: "wss://example.com/ws",
connectHeaders: {
Authorization: `Bearer ${token}`
},
debug: (str) => {
console.log(str);
},
reconnectDelay: 5000
});
client.activate();
На сервере этот заголовок извлекается из STOMP CONNECT
frame и используется для валидации пользователя.
В браузере невозможно надёжно задать произвольные HTTP-заголовки при WebSocket handshake. Это формирует несколько распространённых стратегий передачи токена:
Наиболее корректный вариант при использовании STOMP.js. Токен передаётся уже после установления WebSocket соединения, но до подписок.
const token = localStorage.getItem("access_token");
const client = new Client({
brokerURL: `wss://example.com/ws?token=${encodeURIComponent(token)}`
});
Этот способ используется, когда серверная реализация извлекает токен из URL handshake-запроса. Минус — токен попадает в логи и историю прокси.
При использовании SockJS токен часто передаётся через параметры:
const client = new Client({
webSocketFactory: () =>
new SockJS("https://example.com/ws?token=" + token)
});
SockJS даёт больше гибкости, но усложняет модель маршрутизации и отладки.
После установления WebSocket соединения сервер получает STOMP frame:
CONNECT
accept-version:1.2
host:example.com
Authorization:Bearer eyJhbGciOi...
На стороне сервера (например, Spring Boot + WebSocket MessageBroker) обработка выполняется через интерцептор канала сообщений.
Пример логики:
public class AuthChannelInterceptor implements ChannelInterceptor {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String auth = accessor.getFirstNativeHeader("Authorization");
Authentication user = authenticationService.authenticate(auth);
accessor.setUser(user);
}
return message;
}
}
После успешной аутентификации пользователь привязывается к сессии STOMP.
После установления соединения токен обычно не передаётся повторно, но его содержимое влияет на:
/topic, /queue,
/user)Например, пользователь может иметь доступ только к определённому пространству:
/app/{tenantId}/messages
/topic/{tenantId}/events
Где tenantId извлекается из JWT claims.
Токены доступа часто имеют ограниченное время жизни, поэтому клиент должен учитывать необходимость обновления соединения.
STOMP.js не управляет обновлением токена автоматически, поэтому применяется стратегия пересоздания соединения:
function createClient(token) {
return new Client({
brokerURL: "wss://example.com/ws",
connectHeaders: {
Authorization: `Bearer ${token}`
},
reconnectDelay: 5000
});
}
let client = createClient(getToken());
client.onStompEr ror = (frame) => {
if (frame.headers["message"] === "Token expired") {
refreshToken().then(newToken => {
client.deactivate();
client = createClient(newToken);
client.activate();
});
}
};
Типовые сценарии:
ERROR STOMP frameПример обработки ERROR:
client.onStompEr ror = function (frame) {
console.error("Broker error:", frame.headers["message"]);
console.error("Details:", frame.body);
};
При использовании JWT сервер может проверять срок действия на каждом
критическом действии, включая SUBSCRIBE и
SEND.
В STOMP.js токен часто хранится в браузере, что требует учёта рисков:
localStorage уязвим к XSSsessionStorage ограничивает время жизни, но не защищает
от XSSПример in-memory хранения:
let accessToken = null;
export function setToken(token) {
accessToken = token;
}
export function getToken() {
return accessToken;
}
STOMP.js позволяет обновлять connectHeaders перед
подключением, что важно при refresh token сценариях:
const client = new Client({
brokerURL: "wss://example.com/ws",
connectHeaders: () => ({
Authorization: `Bearer ${getToken()}`
})
});
В некоторых версиях используется beforeConnect:
client.beforeConnect = () => {
client.connectHeaders.Authorization = `Bearer ${getToken()}`;
};
Это позволяет гарантировать актуальность токена при каждом переподключении.
Типичная серверная архитектура предполагает:
/ws/topic, /queueJWT декодируется и преобразуется в Principal, который
затем используется в аннотациях:
@MessageMapping("/chat")
@SendTo("/topic/messages")
public Message handle(Message message, Principal user) {
return service.process(user.getName(), message);
}
Таким образом токен определяет контекст безопасности на всём жизненном цикле сообщения.
STOMP поддерживает концепцию пользовательских очередей:
/user/queue/notifications
Сервер сопоставляет токен с конкретным пользователем и направляет сообщения в приватные каналы.
Пример отправки:
messagingTemplate.convertAndSendToUser(
userId,
"/queue/notifications",
payload
);
Здесь токен фактически становится ключом связывания соединения и пользователя.
При разрыве соединения важно учитывать:
STOMP.js автоматически восстанавливает соединение при
reconnectDelay, но не сохраняет state подписок в сложных
сценариях.
Расширенная логика:
client.onConn ect = () => {
client.subscribe("/topic/events", handler);
};
client.onWebSocketCl ose = () => {
console.log("Connection closed, waiting reconnect");
};
Типовые проблемы:
401 Unauthorized на CONNECTAuthorization header на сервереКаждая из этих проблем проявляется либо в STOMP ERROR frame, либо в закрытии WebSocket соединения.