Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring WebSocket Stomp Exception Handling

I am trying to use Websockets and STOMP 1.2. I want to authenticate user using JWT on CONNECT frame and to return error message if authorization is not valid. Here is channel interceptor

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer
{
    // ... Removed for readability

    @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()))
                {
                    String token = accessor.getFirstNativeHeader(authorizationHeader);
                    if (token != null)
                    {
                        Authentication authentication = tokenUtil.getAuthentication(resolveToken(token));
                        accessor.setUser(authentication);
                    }
                    else
                    {
                        throw new UnauthorizedException();
                    }
                }
                return message;
            }
        });

    }
}

This way the error stack will have

    Failed to send client message to application via MessageChannel in session a63cf95f-4a5b-ef34-b74b-3264ec6dd61f. Sending STOMP ERROR to client.

org.springframework.messaging.MessageDeliveryException: Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]; nested exception is com.example.error.exception.UnauthorizedException: You are not authorized. Please log in.
    at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:146) ~[spring-messaging-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:122) ~[spring-messaging-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.web.socket.messaging.StompSubProtocolHandler.handleMessageFromClient(StompSubProtocolHandler.java:284) ~[spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.web.socket.messaging.SubProtocolWebSocketHandler.handleMessage(SubProtocolWebSocketHandler.java:324) [spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.web.socket.handler.WebSocketHandlerDecorator.handleMessage(WebSocketHandlerDecorator.java:75) [spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.web.socket.handler.LoggingWebSocketHandlerDecorator.handleMessage(LoggingWebSocketHandlerDecorator.java:56) [spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.web.socket.handler.ExceptionWebSocketHandlerDecorator.handleMessage(ExceptionWebSocketHandlerDecorator.java:58) [spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.handleTextMessage(StandardWebSocketHandlerAdapter.java:113) [spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.access$000(StandardWebSocketHandlerAdapter.java:42) [spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter$3.onMessage(StandardWebSocketHandlerAdapter.java:84) [spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter$3.onMessage(StandardWebSocketHandlerAdapter.java:81) [spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.apache.tomcat.websocket.WsFrameBase.sendMessageText(WsFrameBase.java:395) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
    at org.apache.tomcat.websocket.server.WsFrameServer.sendMessageText(WsFrameServer.java:119) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
    at org.apache.tomcat.websocket.WsFrameBase.processDataText(WsFrameBase.java:495) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
    at org.apache.tomcat.websocket.WsFrameBase.processData(WsFrameBase.java:294) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
    at org.apache.tomcat.websocket.WsFrameBase.processInputBuffer(WsFrameBase.java:133) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
    at org.apache.tomcat.websocket.server.WsFrameServer.onDataAvailable(WsFrameServer.java:82) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
    at org.apache.tomcat.websocket.server.WsFrameServer.doOnDataAvailable(WsFrameServer.java:171) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
    at org.apache.tomcat.websocket.server.WsFrameServer.notifyDataAvailable(WsFrameServer.java:151) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
    at org.apache.tomcat.websocket.server.WsHttpUpgradeHandler.upgradeDispatch(WsHttpUpgradeHandler.java:148) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
    at org.apache.coyote.http11.upgrade.UpgradeProcessorInternal.dispatch(UpgradeProcessorInternal.java:54) [tomcat-embed-core-9.0.12.jar:9.0.12]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:53) [tomcat-embed-core-9.0.12.jar:9.0.12]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:770) [tomcat-embed-core-9.0.12.jar:9.0.12]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1415) [tomcat-embed-core-9.0.12.jar:9.0.12]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.12.jar:9.0.12]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_131]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_131]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.12.jar:9.0.12]
    at java.lang.Thread.run(Thread.java:748) [na:1.8.0_131]
Caused by: com.example.error.exception.UnauthorizedException: You are not authorized. Please log in.
    at com.exampleconfig.WebSocketConfig$1.preSend(WebSocketConfig.java:74) ~[classes/:na]
    at org.springframework.messaging.support.AbstractMessageChannel$ChannelInterceptorChain.applyPreSend(AbstractMessageChannel.java:178) ~[spring-messaging-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:132) ~[spring-messaging-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    ... 28 common frames omitted

And similar error will be sent to client end connection will be closed. But after the connection is closed, other stack trace is written as output:

2018-11-11 01:06:48.077 DEBUG 7259 --- [nio-8080-exec-2] s.w.s.h.LoggingWebSocketHandlerDecorator : StandardWebSocketSession[id=a63cf95f-4a5b-ef34-b74b-3264ec6dd61f, uri=/api/ws] closed with CloseStatus[code=1000, reason=null]
2018-11-11 01:06:48.077 DEBUG 7259 --- [nio-8080-exec-2] o.s.w.s.m.SubProtocolWebSocketHandler    : Clearing session a63cf95f-4a5b-ef34-b74b-3264ec6dd61f
2018-11-11 01:06:48.084 DEBUG 7259 --- [nio-8080-exec-2] org.springframework.web.SimpLogging      : Detected unsent DISCONNECT message. Processing anyway.
2018-11-11 01:06:48.085 DEBUG 7259 --- [nio-8080-exec-2] org.springframework.web.SimpLogging      : Processing DISCONNECT session=a63cf95f-4a5b-ef34-b74b-3264ec6dd61f
2018-11-11 01:06:48.086 DEBUG 7259 --- [tboundChannel-1] o.s.w.s.m.SubProtocolWebSocketHandler    : No session for GenericMessage [payload=byte[0], headers={simpMessageType=DISCONNECT_ACK, simpDisconnectMessage=GenericMessage [payload=byte[0], headers={simpMessageType=DISCONNECT, stompCommand=DISCONNECT, simpSessionAttributes={org.springframework.messaging.simp.SimpAttributes.COMPLETED=true}, simpSessionId=a63cf95f-4a5b-ef34-b74b-3264ec6dd61f}], simpSessionId=a63cf95f-4a5b-ef34-b74b-3264ec6dd61f}]
2018-11-11 01:06:48.087  WARN 7259 --- [nio-8080-exec-2] w.s.h.ExceptionWebSocketHandlerDecorator : Unhandled exception after connection closed for ExceptionWebSocketHandlerDecorator [delegate=LoggingWebSocketHandlerDecorator [delegate=SubProtocolWebSocketHandler[StompSubProtocolHandler[v10.stomp, v11.stomp, v12.stomp]]]]

org.springframework.messaging.MessageDeliveryException: Failed to send message to ExecutorSubscribableChannel[clientInboundChannel]; nested exception is org.springframework.security.access.AccessDeniedException: Access is denied
    at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:146) ~[spring-messaging-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:122) ~[spring-messaging-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.web.socket.messaging.StompSubProtocolHandler.afterSessionEnded(StompSubProtocolHandler.java:611) ~[spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.web.socket.messaging.SubProtocolWebSocketHandler.clearSession(SubProtocolWebSocketHandler.java:516) ~[spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.web.socket.messaging.SubProtocolWebSocketHandler.afterConnectionClosed(SubProtocolWebSocketHandler.java:385) ~[spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.web.socket.handler.WebSocketHandlerDecorator.afterConnectionClosed(WebSocketHandlerDecorator.java:85) ~[spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.web.socket.handler.LoggingWebSocketHandlerDecorator.afterConnectionClosed(LoggingWebSocketHandlerDecorator.java:72) ~[spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.web.socket.handler.ExceptionWebSocketHandlerDecorator.afterConnectionClosed(ExceptionWebSocketHandlerDecorator.java:78) ~[spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.onClose(StandardWebSocketHandlerAdapter.java:144) [spring-websocket-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.apache.tomcat.websocket.WsSession.fireEndpointOnClose(WsSession.java:535) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
    at org.apache.tomcat.websocket.WsSession.onClose(WsSession.java:513) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
    at org.apache.tomcat.websocket.WsFrameBase.processDataControl(WsFrameBase.java:347) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
    at org.apache.tomcat.websocket.WsFrameBase.processData(WsFrameBase.java:289) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
    at org.apache.tomcat.websocket.WsFrameBase.processInputBuffer(WsFrameBase.java:133) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
    at org.apache.tomcat.websocket.server.WsFrameServer.onDataAvailable(WsFrameServer.java:82) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
    at org.apache.tomcat.websocket.server.WsFrameServer.doOnDataAvailable(WsFrameServer.java:171) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
    at org.apache.tomcat.websocket.server.WsFrameServer.notifyDataAvailable(WsFrameServer.java:151) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
    at org.apache.tomcat.websocket.server.WsHttpUpgradeHandler.upgradeDispatch(WsHttpUpgradeHandler.java:148) [tomcat-embed-websocket-9.0.12.jar:9.0.12]
    at org.apache.coyote.http11.upgrade.UpgradeProcessorInternal.dispatch(UpgradeProcessorInternal.java:54) [tomcat-embed-core-9.0.12.jar:9.0.12]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:53) [tomcat-embed-core-9.0.12.jar:9.0.12]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:770) [tomcat-embed-core-9.0.12.jar:9.0.12]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1415) [tomcat-embed-core-9.0.12.jar:9.0.12]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.12.jar:9.0.12]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_131]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_131]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.12.jar:9.0.12]
    at java.lang.Thread.run(Thread.java:748) [na:1.8.0_131]
Caused by: org.springframework.security.access.AccessDeniedException: Access is denied
    at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-5.1.1.RELEASE.jar:5.1.1.RELEASE]
    at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:233) ~[spring-security-core-5.1.1.RELEASE.jar:5.1.1.RELEASE]
    at org.springframework.security.messaging.access.intercept.ChannelSecurityInterceptor.preSend(ChannelSecurityInterceptor.java:69) ~[spring-security-messaging-5.1.1.RELEASE.jar:5.1.1.RELEASE]
    at org.springframework.messaging.support.AbstractMessageChannel$ChannelInterceptorChain.applyPreSend(AbstractMessageChannel.java:178) ~[spring-messaging-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    at org.springframework.messaging.support.AbstractMessageChannel.send(AbstractMessageChannel.java:132) ~[spring-messaging-5.1.2.RELEASE.jar:5.1.2.RELEASE]
    ... 26 common frames omitted

I have no idea where AccessDeniedException came from and how can I handle this.

If I do not throw UnauthorizedException in preSend method, client will be connected and when it tries to send something, it will receive AccessDeniedException, because all endpoints needs authenticated user, and then connection will be closed and stack trace for AccessDeniedException printed few times.

What am I doing incorrect here and what is the correct way to handle this kind of use case?

like image 594
Boris Avatar asked Nov 11 '18 00:11

Boris


People also ask

Is STOMP deprecated?

This project is no longer maintained. If you encounter bugs with it or need enhancements, you can fork it and modify it as the project is under the Apache License 2.0.

Does STOMP use WebSocket?

In the Web browser with a custom WebSocketBy default, stomp. js will use the Web browser native WebSocket class to create the WebSocket. However it is possible to use other type of WebSockets by using the Stomp.

What provides support for STOMP as the WebSocket Subprotocol to use in applications?

WebSocket Support. This part of the reference documentation covers Spring Framework's support for WebSocket-style messaging in web applications including use of STOMP as an application level WebSocket sub-protocol.


1 Answers

As solution for AccessDenied exception is that I didn't configured to allow DISCONNECT messages to pass through security. This is my configuration now:

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer
{
    @Override
    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages)
    {
        messages
                .simpTypeMatchers(SimpMessageType.CONNECT,
                        SimpMessageType.DISCONNECT, SimpMessageType.OTHER).permitAll()
                .anyMessage().authenticated();
    }
}

Also, I overrode StompSubProtocolErrorHandler so I could send custom message to my client about error.

Here is example:

@Override
public Message<byte[]> handleClientMessageProcessingError(Message<byte[]>clientMessage, Throwable ex)
{
    Throwable exception = ex;
    if (exception instanceof MessageDeliveryException)
    {
        exception = exception.getCause();
    }

    if (exception instanceof UnauthorizedException)
    {
        return handleUnauthorizedException(clientMessage, exception);
    }

    if (exception instanceof AccessDeniedException)
    {
        return handleAccessDeniedException(clientMessage, exception);
    }

    return super.handleClientMessageProcessingError(clientMessage, ex);
}

...

private Message<byte[]> handleUnauthorizedException(Message<byte[]> clientMessage, Throwable ex)
{
    ApiError apiError = new ApiError(ErrorCodeConstants.UNAUTHORIZED, ex.getMessage());

    return prepareErrorMessage(clientMessage, apiError, ErrorCodeConstants.UNAUTHORIZED_STRING);

}

private Message<byte[]> prepareErrorMessage(Message<byte[]> clientMessage, ApiError apiError, String errorCode)
{
    String message = transformApiErrorToJSONString(apiError);

    StompHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.ERROR);

    setReceiptIdForClient(clientMessage, accessor);
    accessor.setMessage(errorCode);
    accessor.setLeaveMutable(true);

    return MessageBuilder.createMessage(message != null ? message.getBytes() : EMPTY_PAYLOAD, accessor.getMessageHeaders());
}
like image 135
Boris Avatar answered Oct 09 '22 19:10

Boris