Enviando mensagem para usuário específico no Spring Websocket

87

Como enviar mensagem de websocket do servidor apenas para um usuário específico?

Meu webapp tem configuração de segurança Spring e usa websocket. Estou encontrando um problema complicado ao tentar enviar mensagem do servidor apenas para um usuário específico .

Meu entendimento ao ler o manual é do servidor que podemos fazer

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

E do lado do cliente:

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

Mas eu nunca poderia obter o retorno de chamada da assinatura. Eu tentei muitos caminhos diferentes, mas sem sorte.

Se eu enviar para / topic / reply, ele funciona, mas todos os outros usuários conectados também o receberão.

Para ilustrar o problema, criei este pequeno projeto no github: https://github.com/gerrytan/wsproblem

Passos para reproduzir:

1) Clone e construa o projeto (certifique-se de usar o jdk 1.7 e o maven 3.1)

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

2) Acesse http://localhost:8080, faça login usando bob / test ou jim / test

3) Clique em "Solicitar mensagem específica do usuário". Esperado: uma mensagem "olá {nome de usuário}" é exibida ao lado de "Mensagem recebida apenas para mim" apenas para este usuário. Real: nada foi recebido

gerrytan
fonte
Você esteve olhando para convertAndSendToUser (String user, String destination, T message)? docs.spring.io/spring/docs/4.0.0.M3/javadoc-api/org/…
Viktor K.
Sim, tentei isso também, mas sem sorte
gerrytan de
Tenho trabalhado em um projeto privado e esse é o método que usamos e está funcionando para nós. Acho que um problema potencial pode ser que você se inscreveu em "/ usuário / resposta" e está enviando mensagens para "/ usuário / {nome de usuário} / resposta". Eu acho que você deve remover a parte {username} e usar o convertAndSendToUser (String usuário, String destino, mensagem T).
Viktor K.
Obrigado, mas eu tentei simpMessagingTemplate.convertAndSendToUser(principal.getName(), "/user/reply", reply);e quando a mensagem é enviada do servidor ele lança esta exceçãojava.lang.IllegalArgumentException: Expected destination pattern "/principal/{userId}/**"
gerrytan
@ViktorK. está certo e você estava bem perto da solução certa. Sua assinatura do lado do cliente estava correta, você simplesmente tinha que tentar:convertAndSendToUser(principal.getName(), "/reply", reply);
Tip-Sy

Respostas:

85

Oh, o client side no need to known about current userservidor fará isso por você.

No lado do servidor, usando a seguinte maneira de enviar mensagem a um usuário:

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

Nota: Usando queue, não topic, Spring sempre usando queuecomsendToUser

Do lado do cliente

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

Explicar

Quando qualquer conexão de websocket é aberta, Spring irá atribuir a ela um session id(não HttpSession, atribuir por conexão). E quando seu cliente se inscreve em um canal começa com /user/, por /user/queue/replyexemplo:, sua instância de servidor se inscreve em uma fila chamadaqueue/reply-user[session id]

Quando usar enviar mensagem ao usuário, por exemplo: nome de usuário é admin Você escreverásimpMessagingTemplate.convertAndSendToUser("admin", "/queue/reply", message);

O Spring determinará qual session idmapeado para o usuário admin. Ex: Encontrou duas sessões wsxedc123e thnujm456, o Spring irá traduzir para 2 destinos queue/reply-userwsxedc123e queue/reply-userthnujm456, e enviará sua mensagem com 2 destinos para o seu corretor de mensagens.

O agente de mensagens recebe as mensagens e as fornece de volta para a instância do servidor que mantém a sessão correspondente a cada sessão (as sessões do WebSocket podem ser mantidas por um ou mais servidores). O Spring irá traduzir a mensagem para destination(por exemplo user/queue/reply:) e session id(por exemplo wsxedc123:). Em seguida, ele envia a mensagem para o correspondenteWebsocket session

Thanh Nguyen Van
fonte
7
Você pode explicar como sabe o nome de usuário? no seu exemplo você disse que o nome de usuário é 'admin', você recebe o nome de usuário do usuário?
Jorj
1
O nome de usuário foi obtido HttpSessionao iniciar uma conexão de websocket
Thanh Nguyen Van
1
por favor, você poderia fornecer mais informações sobre como definir o nome de usuário ?. Existe alguma maneira de enviar o nome de usuário na mensagem de inscrição?
Andres
1
@Andres: Você pode estender DefaultHandshakeHandlere substituir o métododetermineUser
Thanh Nguyen Van
1
Sua explicação funciona muito bem. Porém, onde está a queue/reply-user[session id]parte no documento oficial?
hbrls
35

Ah eu descobri qual era o meu problema. Primeiro eu não registrei o /userprefixo no corretor simples

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

Então, não preciso do /userprefixo extra ao enviar:

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

O Spring irá anexar automaticamente "/user/" + principal.getName()ao destino, portanto, ele é resolvido em "/ user / bob / reply".

Isso também significa que em javascript eu tive que me inscrever em endereços diferentes por usuário

stompClient.subscribe('/user/' + userName + '/reply,...) 
gerrytan
fonte
3
Mmm ... Existe uma maneira de evitar configurar o userName do lado do cliente? Alterando esse valor (por exemplo, usando outro nome de usuário), você poderá ver as mensagens de outras pessoas.
vdenotaris,
2
Veja minha solução: stackoverflow.com/questions/25646671/…
vdenotaris
como se inscrever para responder ao usuário a partir de métodos anotados com @RequestMappinge também @MessageMappingveja aqui Como se inscrever usando a integração Sping Websocket em userName específico (userId) + obter notificações de método anotado com @RequestMapping?
Shantaram Tupe
Ei meu amigo você pode me dizer isso? Afinal, o que é esse "usuário"? Estou usando Spring Security e meus usuários (nome de usuário) são endereços de e-mail. Quando cada usuário efetua login, ele obtém seu token JWT no Spring Security.
Francisco Souza
Enfrentei os mesmos problemas, pesquisei horas antes de chegar à sua resposta. SÓ a sua explicação me ajudou. Muito obrigado!!
Navaneeth
3

Eu criei um projeto de websocket de amostra usando STOMP também. O que estou percebendo é que

@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();
}

}

funciona independentemente de "/ user" estar ou não incluído em config.enableSimpleBroker (...

Kans
fonte
2

Minha solução disso é baseada na melhor explicação de Thanh Nguyen Van, mas, além disso, configurei MessageBrokerRegistry:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/queue/", "/topic/");
        ...
    }
    ...
}
Nikolay Shabak
fonte
2

Fiz exatamente o mesmo e está funcionando sem usar o usuário

@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");
    }
}
Sid
fonte
Ei, onde você usa isso /app? Nesse caso?
Francisco Souza