104

How to send websocket message from server to specific user only?

My webapp has spring security setup and uses websocket. I'm encountering tricky problem trying to send message from server to specific user only.

My understanding from reading the manual is from the server we can do

simpMessagingTemplate.convertAndSend("/user/{username}/reply", reply);

And on the client side:

stompClient.subscribe('/user/reply', handler);

But I could never get the subscription callback invoked. I have tried many different path but no luck.

If I send it to /topic/reply it works but all other connected users will receive it too.

To illustrate the problem I've created this small project on github: https://github.com/gerrytan/wsproblem

Steps to reproduce:

1) Clone and build the project (make sure you're using jdk 1.7 and maven 3.1)

$ git clone https://github.com/gerrytan/wsproblem.git
$ cd wsproblem
$ mvn jetty:run

2) Navigate to http://localhost:8080, login using either bob/test or jim/test

3) Click "Request user specific msg". Expected: a message "hello {username}" is displayed next to "Received Message To Me Only" for this user only, Actual: nothing is received

gerrytan
  • 40,313
  • 9
  • 84
  • 99
  • Have you been looking at convertAndSendToUser(String user, String destination, T message) ? http://docs.spring.io/spring/docs/4.0.0.M3/javadoc-api/org/springframework/messaging/simp/SimpMessagingTemplate.html#convertAndSendToUser%28java.lang.String,%20java.lang.String,%20T%29 – Viktor K. Mar 13 '14 at 01:46
  • 1
    Yes tried that too but no luck – gerrytan Mar 13 '14 at 01:47
  • I've been working on private project and this is the method that we use and it is working for us. I think that one potential problem could be that you subscribe to "/user/reply" and you are sending messages to "/user/{username}/reply". I think that you should remove the {username} part and use the convertAndSendToUser(String user, String destination, T message). – Viktor K. Mar 13 '14 at 02:00
  • Thanks but I tried `simpMessagingTemplate.convertAndSendToUser(principal.getName(), "/user/reply", reply);` and when the message is sent from server it throws this exception `java.lang.IllegalArgumentException: Expected destination pattern "/principal/{userId}/**"` – gerrytan Mar 13 '14 at 02:07
  • @ViktorK. is right, and you were quite close to the right solution. Your subscription on client side was correct, you simply had to try: `convertAndSendToUser(principal.getName(), "/reply", reply);` – Tip-Sy Oct 15 '14 at 08:58

6 Answers6

105

Oh, client side no need to known about current user, server will do that for you.

On server side, using following way to send message to a user:

simpMessagingTemplate.convertAndSendToUser(username, "/queue/reply", message);

Note: Using queue, not topic, Spring always using queue with sendToUser

On client side

stompClient.subscribe("/user/queue/reply", handler);

Explain

When any websocket connection is open, Spring will assign it a session id (not HttpSession, assign per connection). And when your client subscribe to an channel start with /user/, eg: /user/queue/reply, your server instance will subscribe to a queue named queue/reply-user[session id]

When use send message to user, eg: username is admin You will write simpMessagingTemplate.convertAndSendToUser("admin", "/queue/reply", message);

Spring will determine which session id mapped to user admin. Eg: It found two session wsxedc123 and thnujm456, Spring will translate it to 2 destination queue/reply-userwsxedc123 and queue/reply-userthnujm456, and it send your message with 2 destinations to your message broker.

The message broker receive the messages and provide it back to your server instance that holding session corresponding to each session (WebSocket sessions can be hold by one or more server). Spring will translate the message to destination (eg: user/queue/reply) and session id (eg: wsxedc123). Then, it send the message to corresponding Websocket session

starball
  • 20,030
  • 7
  • 43
  • 238
Thanh Nguyen Van
  • 1,923
  • 1
  • 18
  • 18
  • 8
    Can you please explain how you know the username? in your example you said username is 'admin', you receive the username from user? – Jorj Dec 23 '16 at 09:04
  • 1
    Username was obtained from `HttpSession` when initiate a websocket connection – Thanh Nguyen Van Dec 25 '16 at 03:50
  • 2
    please could you give more information about how to set the username?. is there anyway i can send the username on suscription message? – Andres May 03 '17 at 01:41
  • 1
    @Andres: You can extends `DefaultHandshakeHandler` and override method `determineUser` – Thanh Nguyen Van May 03 '17 at 03:28
  • @Andres: Send username on subscription is possible but very insecure. Please don't! – Thanh Nguyen Van May 03 '17 at 03:30
  • Elaborated on using `determineUser` & `DefaultHandshakeHandler` here : https://stackoverflow.com/questions/37853727/where-user-comes-from-in-convertandsendtouser-works-in-sockjsspring-websocket – Siddharth Garg Dec 23 '17 at 22:40
  • 2
    Your explanation works great. However where is the `queue/reply-user[session id]` part in official document? – hbrls Dec 27 '17 at 02:21
  • @ThanhNguyenVan I am following your answer , would you like to look at my question ? thanks . https://stackoverflow.com/questions/49748468/send-notification-to-specific-user-in-spring-boot-websocket – naila naseem Apr 11 '18 at 04:49
  • I agree with @hbrls, according to the code `super.convertAndSend(this.destinationPrefix + user + destination, payload, headers, postProcessor);`, they do destinationPrefix, then user, THEN destination. Not sure where the "-user" and stuff is being added? Perhaps different version? – Raid Mar 15 '22 at 19:25
  • Hello how do we close the connection. I am getting a warning The web application [ROOT] appears to have started a thread named [MessageBroker-4] but has failed to stop it. This is very likely to create a memory leak. The web application [ROOT] appears to have started a thread named [clientInboundChannel-15] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread: – krr Aug 04 '23 at 06:19
37

Ah I found what my problem was. First I didn't register the /user prefix on the simple broker

<websocket:simple-broker prefix="/topic,/user" />

Then I don't need the extra /user prefix when sending:

convertAndSendToUser(principal.getName(), "/reply", reply);

Spring will automatically prepend "/user/" + principal.getName() to the destination, hence it resolves into "/user/bob/reply".

This also means in javascript I had to subscribe to different address per user

stompClient.subscribe('/user/' + userName + '/reply,...) 
gerrytan
  • 40,313
  • 9
  • 84
  • 99
  • 8
    Mmm... Is there a way to avoid to setup the userName client-side? By changing that value (for example by using another username), you will be able to see messages of others. – vdenotaris Sep 03 '14 at 13:52
  • 2
    See my solution: http://stackoverflow.com/questions/25646671/check-auth-while-sending-a-message-to-a-specific-user-by-using-stomp-and-websock/25647822#25647822 – vdenotaris Sep 03 '14 at 18:55
  • how **subscribe** for to reply to user from methods annotated with `@RequestMapping` and also `@MessageMapping` see here [How to subscribe using Sping Websocket integration on specific userName(userId) + get notifications from method anotated with @RequestMapping?](https://stackoverflow.com/q/47628608/3425489) – Shantaram Tupe Dec 04 '17 at 10:21
  • Hey my friend can you tell me this? What is this "user" anyway? I am using Spring Security and my users (username) are email addresses. When each user logs in he gets his JWT token on Spring Security. – Francisco Souza Oct 14 '19 at 20:37
  • I faced same problems, searchedfor hours before coming to your answer. ONLY your explination helped me. Thank you so much!! – Navaneeth Jul 15 '20 at 07:18
3

I created a sample websocket project using STOMP as well. What I am noticing is that

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
    config.enableSimpleBroker("/topic", "/queue");// including /user also works
    config.setApplicationDestinationPrefixes("/app");
}

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

it works whether or not "/user" is included in config.enableSimpleBroker(...

Ahmed Ashour
  • 5,179
  • 10
  • 35
  • 56
Kans
  • 382
  • 3
  • 17
2

My solution of that based on Thanh Nguyen Van's best explanation, but in addition I have configured MessageBrokerRegistry:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/queue/", "/topic/");
        ...
    }
    ...
}
Nikolay Shabak
  • 576
  • 1
  • 7
  • 18
2

Exactly i did the same and it is working without using user

@Configuration
@EnableWebSocketMessageBroker  
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
       registry.addEndpoint("/gs-guide-websocket").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic" , "/queue");
        config.setApplicationDestinationPrefixes("/app");
    }
}
Thomas Fritsch
  • 9,639
  • 33
  • 37
  • 49
Sid
  • 137
  • 8
1

In the following solution, I wrote code snippets for both the client and backend sides. We need to put /user at the start of the socket's topic for client code. Otherwise, the client can not listen to the socket. Dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

WebSocketConfig.java

package com.oktaykcr.notificationservice.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/socket").setAllowedOriginPatterns("*");
        registry.addEndpoint("/socket").setAllowedOriginPatterns("*").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/file");
        registry.setApplicationDestinationPrefixes("/app");
    }
}

WebSocketContoller.java

package com.oktaykcr.notificationservice.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

@Controller
public class WebSocketController {

    Logger logger = LoggerFactory.getLogger(WebSocketController.class);

    @MessageMapping("/socket")
    @SendTo("/file/status")
    public String fileStatus(@Payload String message) {
        logger.info(message);
        return message;
    }
}

You can send message to socket from anywhere. In my case my principal.getName() is equal to userId.

simpMessagingTemplate.convertAndSendToUser(userId, "/file/status", socketMessage);

Client (ReactJs with react-stomp) App.js

import './App.css';
import SockJsClient from 'react-stomp'
import { useRef } from 'react';

function App() {

  const clientRef = useRef();

  const sendMessage = (msg) => {
    clientRef.current.sendMessage('/app/socket', msg);
  }

  return (
    <div className="App">
      <div>
        <button onClick={() => sendMessage("Hola")}>Send</button>
      </div>
      <SockJsClient url='http://localhost:9090/notification-service/socket' topics={['/user/file/status']}
        onMessage={(msg) => { console.log(msg); }}
        ref={(client) => { clientRef.current = client }} />
    </div>
  );
}

export default App;

The url attribute of the SockJsClient element is http://localhost:9090/notification-service/socket. http://localhost:9090 is Api Gateway ip address, notification-service is name of the microservice and /socket is defined in the WebSocketConfig.java.

oktaykcr
  • 346
  • 6
  • 12