45

I would like to understand how convertAndSendToUser works in Spring SockJS+Websocket framework.

In client, we would connect as

stompClient.connect(login, password, callback())

which will result in connect request with "Stomp credentials" of login and password, that can be seen e.g. if we handle SessionConnectEvent http://www.sergialmar.com/2014/03/detect-websocket-connects-and-disconnects-in-spring-4/

But it remains unclear to me whether this will be the "user" meant in server-side send operation to a queue:

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

The closest I can get is to read this thread Sending message to specific user on Spring Websocket, answer by Thanh Nguyen Van, but it is still unclear.

Basically what I need to do, is to subscribe some clients to same topic, but on server, send them different data. Client may supply user identifier.

Community
  • 1
  • 1
onkami
  • 8,791
  • 17
  • 90
  • 176
  • Did you get a solution for your problem? I am also facing the same issue and looking for a solution – BiJ Apr 04 '17 at 08:46
  • You should have spring security configured for each connection or use @Siddharth's solution for unknown users which is also best solution i have ever seen. – Aliy Jan 14 '20 at 06:15

3 Answers3

79

We know we can send messages to the client from a stomp server using the topic prefixes that he is subscribed to e.g. /topic/hello. We also know we can send messages to a specific user because spring provides the convertAndSendToUser(username, destination, message) API. It accepts a String username which means if we somehow have a unique username for every connection, we should be able to send messages to specific users subscribed to a topic.

What's less understood is, where does this username come from ?

This username is part of a java.security.Principal interface. Each StompHeaderAccessor or WebSocketSession object has instance of this principal and you can get the user name from it. However, as per my experiments, it is not generated automatically. It has to be generated manually by the server for every session.

To use this interface first you need to implement it.

class StompPrincipal implements Principal {
    String name

    StompPrincipal(String name) {
        this.name = name
    }

    @Override
    String getName() {
        return name
    }
}

Then you can generate a unique StompPrincipal for every connection by overriding the DefaultHandshakeHandler. You can use any logic to generate the username. Here is one potential logic which uses UUID :

class CustomHandshakeHandler extends DefaultHandshakeHandler {
    // Custom class for storing principal
    @Override
    protected Principal determineUser(
        ServerHttpRequest request,
        WebSocketHandler wsHandler,
        Map<String, Object> attributes
    ) {
        // Generate principal with UUID as name
        return new StompPrincipal(UUID.randomUUID().toString())
    }
}

Lastly, you need to configure your websockets to use your custom handshake handler.

@Override
void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
    stompEndpointRegistry
         .addEndpoint("/stomp") // Set websocket endpoint to connect to
         .setHandshakeHandler(new CustomHandshakeHandler()) // Set custom handshake handler
         .withSockJS() // Add Sock JS support
}

That's It. Now your server is configured to generate a unique principal name for every connection. It will pass that principal as part of StompHeaderAccessor objects that you can access through connection event listeners, MessageMapping functions etc...

From event listeners :

@EventListener
void handleSessionConnectedEvent(SessionConnectedEvent event) {
    // Get Accessor
    StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage())
}

From Message Mapped APIs

@MessageMapping('/hello')
protected void hello(SimpMessageHeaderAccessor sha, Map message) {
    // sha available in params
}

One last note about using convertAndSendToUser(...). When sending messages to a user, you will use something like this

convertAndSendToUser(sha.session.principal.name, '/topic/hello', message)

However, for subscribing the client, you will use

client.subscribe('/user/topic/hello', callback)

If you subscribe the client to /topic/hello you will only receive broadcasted messages.

Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121
Siddharth Garg
  • 1,560
  • 14
  • 19
  • 1
    From where to access `userName` if I do not have `@MessageMapping`. Say for example I want to use `convertAndSendToUser(...)` in my regular methods? – Naanavanalla Sep 09 '18 at 09:47
  • could you fill in the detail of where `sha.session.principal.name` comes from with `convertAndSendToUser(...)` – denov Sep 19 '18 at 06:45
  • Hi @Siddharth, so is this the approach that works when you don't have Spring Security on the class path or do you still have spring security enabled? – M_K Sep 23 '18 at 09:39
  • @SandeshaJ save the user name when handleSessionConnectedEventin() has been triggered in a map or cache like Redis so that convertAndSendToUser can access when sending. – M_K Sep 23 '18 at 10:07
  • 1
    I cannot access session from sha. I get `session cannot be resolved or is not a field` error. – Kshitij Bajracharya Mar 26 '19 at 09:51
  • Is there anyway to access the Principal in determineUser of the CustomHandshakeHandler? Or any way to get the `sha` from within a service? – Kshitij Bajracharya Mar 28 '19 at 12:21
  • All of this code is not required, if you have security enabled in Spring. Then, when an authenticated user connects via websocket, you can get all info from injected beans in controller class: `org.springframework.messaging.simp.stomp.StompHeaderAccessor` will give you access to the current user that is sending a message to the server (Spring's `Principal`), `org.springframework.messaging.simp.user.SimpUserRegistry` will give you all users currently connected via websocket. – Jakub Kvba May 16 '20 at 11:20
2

I did not do any specific configuration and I can just do this:

@MessageMapping('/hello')
protected void hello(Principal principal, Map message) {
    String username = principal.getName();
}
Wenneguen
  • 3,196
  • 3
  • 13
  • 23
  • Not exactly what the OP asked for, but worked for me. I wanted to extract the sender in the @messagemapping method. Handling this inside `HttpHandshakeInterceptor` is tough since it has a `ServerHttpRequest` object in its constructor. – YetAnotherBot Jan 14 '19 at 07:19
  • 1
    Is there anyway to access the Principal in determineUser of the CustomHandshakeHandler? – Kshitij Bajracharya Mar 28 '19 at 12:21
0

Similar to Wenneguen I was able to do just by injecting Principal in the MessageMapping method

public void processMessageFromClient(@Payload String message, Principal principal) {

the principal.getName() implementation is from org.springframework.security.authentication.UsernamePasswordAuthenticationToken class

Amal K
  • 4,359
  • 2
  • 22
  • 44