Spring session + Spring web socket. Send message to specific client based on session id
Asked Answered
L

3

4

I have followed Quetion1 and Quetion2 from stack overflow to send messages to specific client, based on its sessionId but could not find success.

Below is my sample RestController class

@RestController
public class SpringSessionTestApi {

@Autowired
public SimpMessageSendingOperations messagingTemplate;

@MessageMapping("/messages")
public void greeting(HelloMessage message, SimpMessageHeaderAccessor headerAccessor) throws Exception {

    String sessionId  = (String) headerAccessor.getSessionAttributes().get("SPRING.SESSION.ID");
    messagingTemplate.convertAndSendToUser(sessionId,"/queue/test",message, createHeaders(sessionId));

   }

private MessageHeaders createHeaders(String sessionId) {
    SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
    headerAccessor.setSessionId(sessionId);
    headerAccessor.setLeaveMutable(true);
    return headerAccessor.getMessageHeaders();
   }
}

Session Id: when client sends createSession request, new spring sessionId is generated and same is stored in MongoDB as well. After that when client sends web socket connect request, same sessionId is received which was stored in mongoDb as expected. Till This everything is working fine.

Now my job is to send response back to the client based on the sessionId. For that I have below web socket class:

@Configuration
@EnableScheduling
@EnableWebSocketMessageBroker
public class WebSocketConfig extends
    AbstractSessionWebSocketMessageBrokerConfigurer<ExpiringSession> {

@Override
protected void configureStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/messages");
}

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

and the sample client code that I am using to connect is:

function connect() {

stompClient = Stomp.client('ws://localhost:8016/messages');
stompClient.debug = null;

stompClient.connect({}, function (frame) {
    setConnected(true);
    console.log('Connected: ' + frame);
    stompClient.subscribe('/user/queue/test', function (greeting) {
        console.log("Hello "+greeting);
        console.log("Greeting body "+JSON.parse(greeting.body));

    });
});
}

Please help, Where I am doing wrong in this? Thanks in Advance!

Levalloisian answered 21/4, 2017 at 7:28 Comment(3)
I only see config, Don't see class which should send response to userCoxalgia
@Sarief As per my knowledge "convertAndSendToUser()" method sends response to specific user. Sorry I did not understand what are you asking about?Levalloisian
hm. did not see it. You need to use username, not session id. It uses spring security and looks for user by username of Authority.class. This being said, is your user persisted in spring security?Coxalgia
B
0

If you are using /user channel as you do, try to pass the user as stated here.

@MessageMapping("/messages")
public void greeting(HelloMessage message, SimpMessageHeaderAccessor headerAccessor, Principal principal) 
    throws Exception {
    messagingTemplate.convertAndSendToUser(principal.getName(), "/queue/test", message);
}
Bubonocele answered 21/4, 2017 at 16:39 Comment(1)
thanx for ur reply. I think principal.getName() will provide me the user name that was set by spring security during authentication process. In my case there is no authentication. I just want to reply to client based on its sessionId which i get by headerAccessor.getSessionAttributes().get("SPRING.SESSION.ID").Levalloisian
E
0

I've found a full workable Spring Stomp Chat project in git, the link is here. You can refer to it. https://gist.github.com/theotherian/9906304

Exposition answered 1/7, 2017 at 8:56 Comment(0)
L
0

I was able to solve this by registering a HandshakeHandler that creates a Principal with the session ID as its name:

@Configuration
@EnableWebSocketMessageBroker
class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {

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

   @Override
   public void registerStompEndpoints(StompEndpointRegistry registry) {
      registry.addEndpoint("/messages").setHandshakeHandler(new AnonymousPrincipalHandshakeHandler());
   }

   private static final class AnonymousPrincipalHandshakeHandler extends DefaultHandshakeHandler {

      @Override
      protected Principal determineUser(ServerHttpRequest request,
                                        WebSocketHandler wsHandler,
                                        Map<String, Object> attributes) {
         if (request instanceof ServletServerHttpRequest r) {
            var session = r.getServletRequest().getSession(false);
            return session == null ? null : new AnonymousPrincipal(session.getId());
         }
         return null;
      }

   }

   private record AnonymousPrincipal(String sessionId) implements Principal {

      @Override
      public String getName() {
         return sessionId;
      }

   }

}

I'm then able to send messages to a specific unauthenticated client subscribing to e.g. /user/queue/test by doing:

messagingTemplate.convertAndSendToUser(sessionId, "/queue/test", message);

I'm not sure if there are any security concerns with using the session ID like this. Alternatively you could auto-generate a username and store it as a session attribute, use it as the name of the Principal, and look it up when you want to send a message to clients with the given session. In OP's case you would also need to copy this session attribute to the websocket session in a HandshakeInterceptor since you don't have access to the HttpSession in an @MessageMapping method.

Loidaloin answered 1/8, 2023 at 11:38 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.