概述

在这个教程中,我们将介绍如何使用Spring WebSockets向单个用户发送STOMP消息。这很重要,因为我们有时不想将每条消息广播给每个用户。此外,我们还将展示如何以安全的方式发送这些消息。

对于WebSockets的基础知识,请参考这篇入门教程,了解如何快速上手。对于更深入的安全性探讨,可以阅读这篇文章,以保护你的WebSockets实现。

2. 队列、主题和端点

在Spring WebSockets和STOMP中,有三种主要方式来指定消息发送的目的地以及订阅方式:

  1. 主题(Topics) - 公开的对话或聊天主题,对任何客户端或用户开放。
  2. 队列(Queues) - 专为特定用户及其当前会话预留。
  3. 端点(Endpoints) - 通用端点。

现在,让我们快速了解一下每个示例的上下文路径:

  • /topic/movies
  • /user/queue/specific-user
  • /secured/chat

值得注意的是,为了向特定用户发送消息,我们必须使用队列,因为主题和端点不支持这种功能

3. 配置

现在,让我们学习如何配置应用程序,以便向特定用户发送消息:

public class SocketBrokerConfig extends 
  WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/secured/history","/secured/user/queue/specific-user");
        config.setApplicationDestinationPrefixes("/spring-security-mvc-socket");
        config.setUserDestinationPrefix("/secured/user");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/secured/room").withSockJS();
    }
}

确保包含用户目的地非常重要,因为它决定了哪些端点是为单个用户保留的。

我们还将在所有队列和用户目的地前加上/secured前缀,以要求身份验证。对于不受保护的端点,我们可以去掉/secured前缀(由于我们的其他安全设置)。

pom.xml的角度来看,不需要额外的依赖。

4. URL 映射

我们希望客户端使用以下模式的URL映射来订阅队列:

"/user/queue/updates"

这个映射将由UserDestinationMessageHandler自动转换为我们已经在客户端订阅的用户会话特定地址。

例如,如果我们有一个名为"user123"的用户,对应的地址将是:

"/queue/updates-user123"

在服务器端,我们将使用以下URL映射模式发送用户特定响应:

"/user/{username}/queue/updates"

这也会被转换为我们已经在客户端订阅的正确URL映射。

因此,关键在于两点:

  1. 在指定的用户目的地前缀(在AbstractWebSocketMessageBrokerConfigurer中配置)。
  2. URL映射中包含/queue

在下一节中,我们将详细介绍如何做到这一点。

5. 调用convertAndSendToUser()

我们可以从SimpMessagingTemplateSimpMessageSendingOperations非静态地调用convertAndSendToUser()

@Autowired
private SimpMessagingTemplate simpMessagingTemplate;

@MessageMapping("/secured/room") 
public void sendSpecific(
  @Payload Message msg, 
  Principal user, 
  @Header("simpSessionId") String sessionId) throws Exception { 
    OutputMessage out = new OutputMessage(
      msg.getFrom(), 
      msg.getText(),
      new SimpleDateFormat("HH:mm").format(new Date())); 
    simpMessagingTemplate.convertAndSendToUser(
      msg.getTo(), "/secured/user/queue/specific-user", out); 
}

你可能已经注意到:

@Header("simpSessionId") String sessionId

@Header注解允许访问由入站消息暴露的头信息。例如,我们可以无需复杂的拦截器直接获取当前的sessionId。同样,我们可以通过Principal访问当前用户。

重要的是,本文所采用的方法相对于@sendToUser注解提供了更大的URL映射定制能力。有关更多关于该注解的信息,请参阅这篇优秀文章

在客户端,我们将使用JavaScript的connect()方法初始化一个SockJS实例,并使用STOMP连接到我们的WebSocket服务器:

var socket = new SockJS('/secured/room'); 
var stompClient = Stomp.over(socket);
var sessionId = "";

stompClient.connect({}, function (frame) {
    var url = stompClient.ws._transport.url;
    url = url.replace(
      "ws://localhost:8080/spring-security-mvc-socket/secured/room/",  "");
    url = url.replace("/websocket", "");
    url = url.replace(/^[0-9]+\//, "");
    console.log("Your current session is: " + url);
    sessionId = url;
}

我们还会访问提供的sessionId并将它附加到"secured/room"的URL映射中。这使我们能够动态且手动提供用户特定的订阅队列:

stompClient.subscribe('secured/user/queue/specific-user' 
  + '-user' + that.sessionId, function (msgOut) {
     //handle messages
}

设置完成后,我们应该看到:

特定用户的特定Socket

而在服务器控制台中:

特定Socket的终端

6. 总结

有关此主题的更多信息,请查看Spring官方博客官方文档

一如既往,本文中的代码示例可在GitHub上找到。