Where “user” comes from in convertAndSendToUser works in SockJS+Spring Websocket?

余生长醉 提交于 2019-11-28 18:21:26

I have been trying to understand websockets and came across this question. Was thoroughly disappointed not to find an answer. Here's something for future reader.

I will assume the reader has a basic understanding of spring websockets using Stomp. Terms like subscription, destination prefixes, topics, socket configuration file etc. are understood.

We know we can send messages to 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 StomHeaderAccessor 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.

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();
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!