1

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?

Wais Kamal
  • 5,858
  • 2
  • 17
  • 36

2 Answers2

0

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;
            }
        });
    }
}

isatori
  • 146
  • 1
  • 8
  • So the only way right now is to have the server assign a random identifier to a user when they connect, then authenticate the user via a normal message. Do I get it right? – Wais Kamal Mar 22 '22 at 06:47
0

If you are looking to get the authorization header value and put it in the security context, you can do the following:

 @MessageMapping("/send")
    public Comment comment(@Payload Comment comment, @Headers Map<String, Object> messageHeaders) {
        Map<String, List<String>> headers = (Map<String, List<String>>) messageHeaders.get("nativeHeaders");
        String token = headers.get(HttpHeaders.AUTHORIZATION).get(0);
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7);
            // get username from token
            DecodedJWT decodedJWT = JwtUtils.decodeToken(token);
            String username = decodedJWT.getSubject();
            // find user by username
            User user = userRepo.findByUsername(username);
            // set user to security context
            UsernamePasswordAuthenticationToken authentication =
                    new UsernamePasswordAuthenticationToken(user, null);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        Comment commentSave = commentRepo.save(comment);
        simpMessagingTemplate.convertAndSend("/post/" + comment.getPost().getId() + "/comments", comment);
        return comment;
    }

Front end you need to include header when sending message instead of connecting to websocket

const headers = {
    Authorization: 'Bearer ' + access_token,
};
stompClient.send('/comment/send', headers, JSON.stringify(payload));