如何全局处理Spring WebSockets / Spring Messaging异常?

时间:2018-02-08 15:15:40

标签: java spring spring-security spring-websocket spring-messaging

问题
有没有办法在Spring WebSocket模块中全局处理由错误(通常不充分的权限)引起的Spring Messaging MessageDeliveryException

用例
我已经在STOMP上实现了Spring WebSockets以支持我的webapp中的ws连接。为了保护websocket端点,我创建了拦截器,授权用户在STOMP CONNECT时启动STOMP会话(如Spring文档here in 22.4.11 section中所述):

@Component
public class StompMessagingInterceptor extends ChannelInterceptorAdapter {

    // Some code not important to the problem

    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor headerAccessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);

        switch (headerAccessor.getCommand()) {
            // Authenticate STOMP session on CONNECT using jwt token passed as a STOMP login header - it's working great
            case CONNECT:
                authorizeStompSession(headerAccessor);
                break;
        }

        // Returns processed message
        return message;
    }

    // Another part of code not important for the problem
}

并包含spring-security-messaging配置,以便在发送消息时添加对权限的细粒度控制:

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    @Override
    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages
            .simpTypeMatchers(
                SimpMessageType.CONNECT,
                SimpMessageType.DISCONNECT,
                SimpMessageType.HEARTBEAT
            ).authenticated()
            .simpSubscribeDestMatchers("/queue/general").authenticated()
            .simpSubscribeDestMatchers("/user/queue/priv").authenticated()
            .simpDestMatchers("/app/general").authenticated()
            .simpDestMatchers("/user/*/queue/priv").hasAuthority("ADMIN")
            .anyMessage().denyAll();
    }

    @Override
    protected boolean sameOriginDisabled() {
        return true;
    }
}

首先 - 此配置按预期工作,问题是在websocket通信期间发生某些安全异常(比如说没有管理员权限的用户尝试在“/ user / {something} / queue / priv”端点上发送消息)将在org.springframework.messaging.MessageDeliveryException被提升时结束:

  • 将完整的异常堆栈跟踪写入我的服务器日志
  • 返回包含堆栈跟踪部分的STOMP ERROR帧,因为它是message字段。

我想要做的是捕捉(如果可能的话全局)DeliveryException,检查是什么导致了它并且顺从地创建了我自己的消息以便在STOMP ERROR帧中返回(假设有一些错误代码,比如403模仿HTTP)而不是进一步抛出原始异常只是用我的记录器记录一些警告。有可能吗?

我尝试了什么
在寻找解决方案时,我发现有些人使用@MessageExceptionHandler来捕获消息传递异常,Spring 4.2.3(我使用的版本)文档只提到here in 25.4.11 section一次。我试着像这样使用它:

@Controller
@ControllerAdvice
public class WebSocketGeneralController {

    ...

    @MessageExceptionHandler
    public WebSocketMessage handleException(org.springframework.messaging.MessageDeliveryException e) {
        WebSocketMessage errorMessage = new WebSocketMessage();
        errorMessage.setMessage(e.getClass().getName());
        return errorMessage;
    }
}

但似乎在任何时候都没有调用方法(尝试捕获不同的异常,只包括Exception - 没有结果)。我还应该研究什么?

2 个答案:

答案 0 :(得分:0)

由于来自传递调度程序servlet的请求的@ControllerAdvice catch异常,它不起作用。当您保护端点并且有人发出未经授权的请求时,它不会通过调度程序servlet。请求被弹簧拦截器捕获。

答案 1 :(得分:0)

@ControllerAdvice@MessageExceptionHandler在业务逻辑级别(例如@MessageMappingSimpMessagingTemplate)工作。

要处理STOMP异常,您需要在STOMP注册表中设置STOMP错误处理程序:

@Configuration
@EnableWebSocketMessageBroker
class WebSocketConfiguration : WebSocketMessageBrokerConfigurer {

    override fun configureMessageBroker(registry: MessageBrokerRegistry) {
        // ...
    }

    override fun registerStompEndpoints(registry: StompEndpointRegistry) {
        registry.addEndpoint("/ws")

        // Handle exceptions in interceptors and Spring library itself.
        // Will terminate a connection and send ERROR frame to the client.
        registry.setErrorHandler(object : StompSubProtocolErrorHandler() {
            override fun handleInternal(
                errorHeaderAccessor: StompHeaderAccessor,
                errorPayload: ByteArray,
                cause: Throwable?,
                clientHeaderAccessor: StompHeaderAccessor?
            ): Message<ByteArray> {
                errorHeaderAccessor.message = null
                val message = "..."
                return MessageBuilder.createMessage(message.toByteArray(), errorHeaderAccessor.messageHeaders)
            }
        })
    }
}