Spring 4.x token-based WebSocket SockJS fallback a

2019-07-28 23:00发布

问题:

This question already has an answer here:

  • JSON Web Token (JWT) with Spring based SockJS / STOMP Web Socket 3 answers

Getting Spring Boot 1.4 + Spring 4.x + Spring Security 4.x WebSocket authentication to work with stateless token-based authentication seems to be an adventure!

So far, as I understand it, SockJS is not able to set the Authentication header with the token because browser's do not expose that API to Javascript (see https://github.com/sockjs/sockjs-client/issues/196). I have worked around that by setting the authentication token in a query parameter as suggested on the above issue, and then using a Spring HandshakeHandler with determineUser() to map the query parameter to a User entity. Ugly and less secure, but at least it works for WebSockets.

However, when SockJS falls back to another mechanism e.g. XHR streaming, the same mechanism no longer works. A HandshakeInterceptor has access to the request and can obtain the authentication from the query param, but determineUser on the HandshakeHandler is never called for non-WebSocket handshakes.

The closest I have gotten so far is to bypass the built-in connection-level Spring machinery to determine the authentication. Instead, I set the authentication token at the message-level by setting it in the Stomp headers on the client side e.g.:

stompClient.send("/wherever", {token: 'token'}, ...);

and extract it on the server-side with a channel interceptor:

configureClientInboundChannel(ChannelRegistration registration) {
  registration.setInterceptors(new ChannelInterceptorAdapter() {
     Message<*> preSend(Message<*> message,  MessageChannel channel) {
      StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
      // not documented anywhere but necessary otherwise NPE in StompSubProtocolHandler!
      accessor.setLeaveMutable(true);
      List tokenList = accessor.getNativeHeader("token");
      if(tokenList == null || tokenList.size < 1) {
        return message;
      }
      Principal yourAuth = [...];
      return MessageBuilder.createMessage(message.payload, accessor.messageHeaders)
    }
  })

Now Spring injects the Principal into any controller methods that require it, BUT the user is still not saved to the websocket session, so messages still cannot be sent to a particular user.

How do I get Spring to "see" the authentication extracted from the query parameter?

回答1:

Use the Stomp Headers

Bypass the built-in connection-level Spring machinery to determine the authentication. Instead, set the authentication token at the message-level by setting it in the Stomp headers on the client side. See the approach I outlined here:

https://stackoverflow.com/a/39456274/430128