Search code examples
springspring-websocketstompjs

How do I send headers in the initial handshake with a Spring websocket server?


I set up a Spring WebSocket server with the following handler:

public class HandshakeHandler extends DefaultHandshakeHandler {
    @Override
    protected Principal determineUser(ServerHttpRequest request,
            WebSocketHandler handler, Map<String, Object> attributes) {

        HttpHeaders headers = request.getHeaders();
        System.out.println(headers.toString());
        ...
    }
}

And according to the StompJS docs, I am sending the headers as follows:

let socket = new SockJS("/greetings");
let stompClient = Stomp.over(socket);
stompClient.connect({
  key1: "value1",
  key2: "value2",
  key3: "value3"
}, frame => {
  stompClient.subscribe("/user/queue/greetings", greeting => {
    console.log(greeting.body);
  })
})

However the log does not show the headers. Here is an example of what is logged:

[host:"localhost:8080", connection:"Upgrade", pragma:"no-cache", cache-control:"no-cache", user-agent:"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36", upgrade:"websocket", origin:"http://localhost:8080", sec-websocket-version:"13", accept-encoding:"gzip, deflate, br", accept-language:"en-US,en;q=0.9", sec-websocket-key:"YXYdFogQD4AFDT3gvdSyMg==", sec-websocket-extensions:"permessage-deflate; client_max_window_bits"]

What am I doing wrong?


Solution

  • First of all, your HandshakeHandler code should work as it is. I tried to replicate it by creating an integration test about it as follows (and I was able to get the header values set):

        @BeforeEach
        public void setup() {
            List<Transport> transports = new ArrayList<>();
            transports.add(new WebSocketTransport(new StandardWebSocketClient()));
            this.sockJsClient = new SockJsClient(transports);
            this.stompClient = new WebSocketStompClient(sockJsClient);
            this.stompClient.setMessageConverter(new MappingJackson2MessageConverter());
            this.headers.add("key1", "value1");
        }
    
        @Test
        public void getGreeting() throws Exception {
            StompSessionHandler handler = new StompSessionHandlerAdapter() {
                @Override
                public void afterConnected(final StompSession session, StompHeaders connectedHeaders) {
                    session.subscribe("/user/queue/greetings", new StompFrameHandler() {
                        @Override
                        public Type getPayloadType(StompHeaders headers) {
                            return Greeting.class;
                        }
                        @Override
                        public void handleFrame(StompHeaders headers, Object payload) {
                            Greeting greeting = (Greeting) payload;
                            try {
                                assertEquals("Hello, Spring!", greeting.getContent());
                            } catch (Throwable t) {
                                fail("Greeting not received");
                            } finally {
                                session.disconnect();
                            }
                        }
                    });
                }
            };
    
            this.stompClient.connect("ws://localhost:{port}/greetings", this.headers, handler, this.port);
        }
    
    

    However, the problem lies in SockJS not being able to send authorization in the header, as it was a security issue.

    Reference: https://github.com/sockjs/sockjs-node#authorisation

    There is a workaround though, on a proper way of doing it: JSON Web Token (JWT) with Spring based SockJS / STOMP Web Socket

    But if you still want to access your header set from SockJS, here is as some snippets that I tried and was able to get header values:

    @Configuration
    @EnableWebSocketMessageBroker
    public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
    ...
        @Override
        public void configureClientInboundChannel(ChannelRegistration registration) {
            registration.interceptors(new ChannelInterceptor() {
                @Override
                public Message<?> preSend(Message<?> message, MessageChannel channel) {
                    StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
                    if (accessor != null &&
                            (StompCommand.CONNECT.equals(accessor.getCommand()) ||
                                    StompCommand.SEND.equals(accessor.getCommand()))) {
                        String login = accessor.getLogin();
                        String header2 = accessor.getFirstNativeHeader("key1");
    
                        logger.info("Login value: " + login);
                        logger.info("Some header: " + header2);
                    }
                    return message;
                }
            });
        }
    }