This question already has an answer here:
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?
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