在SockJS+Spring Websocket中,convertAndSendToUser方法中的"user"是从哪里来的?

45

我希望了解在Spring SockJS+Websocket框架中,convertAndSendToUser如何工作。

在客户端,我们会以以下方式连接:

stompClient.connect(login, password, callback())
这将导致使用登录名和密码的“Stomp凭据”进行连接请求,例如在处理SessionConnectEvent时可以看到。但我不确定这是否是服务器端发送操作到队列所指的“用户”。http://www.sergialmar.com/2014/03/detect-websocket-connects-and-disconnects-in-spring-4/
 simpMessagingTemplate.convertAndSendToUser(username, "/queue/reply", message);

我能找到的最接近的是阅读这个主题 在Spring WebSocket中向特定用户发送消息,由Thanh Nguyen Van回答,但仍不清楚。

基本上,我需要做的是订阅一些客户端到相同的主题,但在服务器上向它们发送不同的数据。客户端可能会提供用户标识符。


你解决了你的问题吗?我也遇到了同样的问题,正在寻找解决方案。 - BiJ
你应该为每个连接配置Spring安全性,或者使用@Siddharth的解决方案来处理未知用户,这也是我见过的最好的解决方案。 - Aliy
3个回答

80

我们知道可以使用客户端订阅的主题前缀(例如/topic/hello)向stomp服务器发送消息。我们还知道可以通过Spring提供的convertAndSendToUser(username, destination, message) API向特定用户发送消息。它接受一个字符串用户名,这意味着如果我们对于每个连接都有一个唯一的用户名,我们应该能够向订阅主题的特定用户发送消息。

较少人理解的是,这个用户名是从哪里来的?

这个用户名是java.security.Principal接口的一部分。 每个StompHeaderAccessorWebSocketSession对象都具有此Principal实例,您可以从中获取用户名。但是,根据我的实验,它不会自动生成。服务器必须为每个会话手动生成它。

要使用此接口,首先需要实现它。

class StompPrincipal implements Principal {
    String name

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

    @Override
    String getName() {
        return name
    }
}

您可以通过覆盖DefaultHandshakeHandler来为每个连接生成唯一的StompPrincipal。 您可以使用任何逻辑生成用户名。 这里是一个潜在的逻辑,它使用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())
    }
}

最后,您需要配置您的websockets以使用您的自定义握手处理程序。

@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
}

那就这样了。现在你的服务器已经配置好,可以为每个连接生成一个唯一的主体名。它将把该主体作为StompHeaderAccessor对象的一部分传递,你可以通过连接事件监听器、MessageMapping函数等方式访问它们...

从事件监听器:

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

从消息映射的 API

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

关于使用convertAndSendToUser(...)的最后一点注意事项。当向用户发送消息时,您将使用类似于以下内容的内容

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

然而,要订阅客户端,您将使用

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

如果您将客户端订阅到/topic/hello,那么您只会收到广播的消息。


1
如果我没有@MessageMapping,那么从哪里可以访问userName?例如,假设我想在我的常规方法中使用convertAndSendToUser(...),该怎么办? - Naanavanalla
你可以填写一下 sha.session.principal.name 是从哪里的 convertAndSendToUser(...) 来的吗? - denov
嗨,@Siddharth,如果您的类路径上没有Spring Security,那么这是否是可行的方法?或者您仍然启用了Spring Security? - M_K
@SandeshaJ 在 handleSessionConnectedEventin() 被触发时,将用户名保存在类似 Redis 的映射或缓存中,以便 convertAndSendToUser 发送消息时可以访问。 - M_K
1
我无法从sha访问会话。我收到“session cannot be resolved or is not a field”错误。 - Kshitij Bajracharya
显示剩余2条评论

2
我没有进行任何特定的配置,而我只需要做到这一点:
@MessageMapping('/hello')
protected void hello(Principal principal, Map message) {
    String username = principal.getName();
}

不完全是OP所要求的,但对我有用。我想在@messagemapping方法中提取发送者。在HttpHandshakeInterceptor内处理这个问题很困难,因为它在其构造函数中有一个ServerHttpRequest对象。 - YetAnotherBot
1
在CustomHandshakeHandler的determineUser方法中有没有办法访问Principal? - Kshitij Bajracharya

0

和 Wenneguen 一样,我能够通过将 Principal 注入到 MessageMapping 方法中来完成。

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

principal.getName() 实现来自于 org.springframework.security.authentication.UsernamePasswordAuthenticationToken 类。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接