Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Replying multiple times over web-socket without spring authentication

Note: see update at the bottom of the question for what I eventually concluded.

I need to send multiple responses to a request over the web socket that sent the request message, the first one quickly, and the others after the data is verified (somewhere between 10 and 60 seconds later, from multiple parallel threads).

I am having trouble getting the later responses to stop broadcasting over all open web sockets. How do I get them to only send to the initial web socket? Or should I use something besides Spring STOMP (because, to be honest, all I want is the message routing to various functions, I don't need or want the ability to broadcast to other web sockets, so I suspect I could write the message distributor myself, even though it is reinventing the wheel).

I am not using Spring Authentication (this is being retrofitted into legacy code).

On the initial return message, I can use @SendToUser, and even though we don't have a user, Spring only sends the return value to the websocket that sent the message. (see this question).

With the slower responses, though, I think I need to use SimpMessagingTemplate.convertAndSendToUser(user, destination, message), but I can't, because I have to pass in the user, and I can't figure out what user the @SendToUser used. I tried to follow the steps in this question, but didn't get it to work when not authenticated (principal.getName() returns null in this case).

I've simplified this considerably for the prototype, so don't worry about synchronizing threads or anything. I just want the web sockets to work correctly.

Here is my controller:

@Controller
public class TestSocketController
{
  private SimpMessagingTemplate template;

  @Autowired
  public TestSocketController(SimpMessagingTemplate template)
  {
    this.template = template;
  }

  // This doesn't work because I need to pass something for the first parameter.
  // If I just use convertAndSend, it broacasts the response to all browsers
  void setResults(String ret)
  {
    template.convertAndSendToUser("", "/user/topic/testwsresponse", ret);
  }

  // this only sends "Testing Return" to the browser tab hooked to this websocket
  @MessageMapping(value="/testws")
  @SendToUser("/topic/testwsresponse")
  public String handleTestWS(String msg) throws InterruptedException
  {
    (new Thread(new Later(this))).start();
    return "Testing Return";
  }

  public class Later implements Runnable
  {
    TestSocketController Controller;
    public Later(TestSocketController controller)
    {
        Controller = controller;
    }

    public void run()
    {
        try
        {
            java.lang.Thread.sleep(2000);

            Controller.setResults("Testing Later Return");
        }
        catch (Exception e)
        {
        }
    }
  }
}

For the record, here is the browser side:

var client = null;
function sendMessage()
{
    client.send('/app/testws', {}, 'Test');
}

// hooked to a button
function test()
{
    if (client != null)
    {
        sendMessage();
        return;
    }

    var socket = new SockJS('/application-name/sendws/');
    client = Stomp.over(socket);
    client.connect({}, function(frame)
    {
        client.subscribe('/user/topic/testwsresponse', function(message)
        {
            alert(message);
        });

        sendMessage();
    });
});

And here is the config:

@Configuration
@EnableWebSocketMessageBroker
public class TestSocketConfig extends AbstractWebSocketMessageBrokerConfigurer
{
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config)
    {
        config.setApplicationDestinationPrefixes("/app");
        config.enableSimpleBroker("/queue", "/topic");
        config.setUserDestinationPrefix("/user");
    }

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

UPDATE: Due to the security issues involved with the possibility of information being sent over other websockets than the originating socket, I ended up recommending to my group that we do not use the Spring 4.0 implementation of STOMP over Web Sockets. I understand why the Spring team did it the way they did it, and it is more power then we needed, but the security restrictions on our project were severe enough, and the actual requirements were simple enough, that we decided to go a different way. That doesn't invalidate the answers below, so make your own decision based on your projects needs. At least we have hopefully all learned the limitations of the technology, for good or bad.

like image 874
Guy Schalnat Avatar asked May 04 '15 19:05

Guy Schalnat


1 Answers

Why don't you use a separate topic for each client?

  1. Client generates a session id.

    var sessionId = Math.random().toString(36).substring(7);

  2. Client subscribes to /topic/testwsresponse/{sessionId}, then sends a message to '/app/testws/{sessionId}'.

  3. In your controller you use @MessageMapping(value="/testws/{sessionId}") and remove @SendToUser. You can use @DestinationVariable to access sessionId in your method.
  4. The controller sends further responses to /topic/testwsresponse/{sessionId}.

Essentially Spring does a similar thing internally when you use user destinations. Since you don't use Spring Authentication you cannot rely on this mechanism but you can easily implement your own as I described above.

var client = null;
var sessionId = Math.random().toString(36).substring(7);
function sendMessage()
{
    client.send('/app/testws/' + sessionId, {}, 'Test');
}

// hooked to a button
function test()
{
    if (client != null)
    {
        sendMessage();
        return;
    }

    var socket = new SockJS('/application-name/sendws/');
    client = Stomp.over(socket);
    client.connect({}, function(frame)
    {
        client.subscribe('/topic/testwsresponse/' + sessionId, function(message)
        {
            alert(message);
        });

        // Need to wait until subscription is complete
        setTimeout(sendMessage, 1000);
    });
}); 

Controller:

@Controller
public class TestSocketController
{
    private SimpMessagingTemplate template;

    @Autowired
    public TestSocketController(SimpMessagingTemplate template)
    {
        this.template = template;
    }

    void setResults(String ret, String sessionId)
    {
        template.convertAndSend("/topic/testwsresponse/" + sessionId, ret);
    }

    @MessageMapping(value="/testws/{sessionId}")
    public void handleTestWS(@DestinationVariable String sessionId, @Payload String msg) throws InterruptedException
    {
        (new Thread(new Later(this, sessionId))).start();
        setResults("Testing Return", sessionId);
    }

    public class Later implements Runnable
    {
        TestSocketController Controller;
        String sessionId;
        public Later(TestSocketController controller, String sessionId)
        {
            Controller = controller;
            this.sessionId = sessionId;
        }

        public void run()
        {
            try
            {
                java.lang.Thread.sleep(2000);

                Controller.setResults("Testing Later Return", sessionId);
            }
            catch (Exception e)
            {
            }
        }
    }
}

Just tested it, works as expected.

like image 56
medvedev1088 Avatar answered Oct 12 '22 10:10

medvedev1088