在没有弹簧身份验证的情况下通过 web-socket 多次回复

Posted

技术标签:

【中文标题】在没有弹簧身份验证的情况下通过 web-socket 多次回复【英文标题】:Replying multiple times over web-socket without spring authentication 【发布时间】:2015-05-04 19:19:20 【问题描述】:

注意:请参阅问题底部的更新了解我最终得出的结论。

我需要通过发送请求消息的 Web 套接字对请求发送多个响应,第一个响应很快,其他响应在验证数据后(大约 10 到 60 秒后,来自多个并行线程)。

我无法获得稍后的响应以停止通过所有打开的 Web 套接字进行广播。如何让它们仅发送到初始 Web 套接字?或者我应该使用 Spring STOMP 之外的东西(因为,​​老实说,我想要的只是将消息路由到各种功能,我不需要或想要广播到其他 web 套接字的能力,所以我怀疑我可以写消息经销商本人,即使它正在重新发明***)。

我没有使用 Spring 身份验证(正在改进为遗留代码)。

在初始返回消息上,我可以使用@SendToUser,即使我们没有用户,Spring 也只会将返回值发送到发送消息的 websocket。 (见this question)。

不过,由于响应速度较慢,我想我需要使用 SimpMessagingTemplate.convertAndSendToUser(user, destination, message),但我不能,因为我必须传入用户,我不知道是什么使用 @SendToUser 的用户。我尝试按照in this question 的步骤进行操作,但在未通过身份验证时无法正常工作(在这种情况下,principal.getName() 返回 null)。

我已经为原型大大简化了这一点,所以不用担心同步线程或其他任何事情。我只是希望网络套接​​字能够正常工作。

这是我的控制器:

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

为了记录,这里是浏览器端:

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

这是配置:

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

更新:由于信息通过其他 websockets 发送而不是原始套接字的可能性涉及安全问题,我最终向我的小组建议我们不要使用 Spring 4.0 的 STOMP over Web Sockets 实现。我理解 Spring 团队为什么按照他们的方式做,它比我们需要的更强大,但是对我们项目的安全限制足够严格,实际需求也足够简单,所以我们决定走不同的路.这不会使以下答案无效,因此请根据您的项目需求做出自己的决定。至少我们希望大家都了解了这项技术的局限性,无论好坏。

【问题讨论】:

【参考方案1】:

为什么不为每个客户使用单独的主题?

    客户端生成一个会话 id。

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

    客户端订阅 /topic/testwsresponse/sessionId,然后向“/app/testws/sessionId”发送消息。

    在您的控制器中,您使用@MessageMapping(value="/testws/sessionId") 并删除@SendToUser。您可以使用 @DestinationVariable 在您的方法中访问 sessionId。 控制器向 /topic/testwsresponse/sessionId 发送更多响应。

本质上,当您使用用户目的地时,Spring 在内部会做类似的事情。由于您不使用 Spring 身份验证,因此您不能依赖此机制,但您可以轻松实现自己的,如上所述。

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
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)
            
            
        
    

刚刚测试过,按预期工作。

【讨论】:

我有一些安全问题。首先是有人可能会找出其他人正在使用的随机字符串并收听对话。第二个是某人可以注册一些(或全部,也许)潜在字符串并听取所有响应。对于这个项目来说,即使很幸运并听取其他人的回应也不够安全。我真的只需要通过单个 Web 套接字发送消息,我开始认为 STOMP 的 Spring 实现不会这样做.. 您不必担心安全问题。只需使用足够长的随机字符串或使用 UUID 作为会话 ID。 en.wikipedia.org/wiki/… 基本上黑客平均需要 170 亿年才能破解你的系统。 我猜你的 gmail 密码比黑客猜随机会话 id 的可能性更大。 是的,但是如果他们能以某种方式找到一个人的会话 ID 怎么办?我还没有完全考虑清楚,但我担心,随着所有数据的黑客攻击正在发生,我不得不担心安全性。 我同意你的看法。但是安全问题与我提供的解决方案无关(问题是关于防止消息广播)。安全是一个单独的问题。话虽如此,您只需要使会话 id 足够长,即使没有任何其他身份验证机制,该解决方案也将是完全安全的,只要您使用 WSS(WebSocket secure: WS over TLS)【参考方案2】:

这不是完整的答案。只是一般的考虑和建议。 你不能通过同一个套接字做不同的事情或连接类型。为什么不针对不同的工作使用不同的插座?有些有身份验证,有些没有。有些用于快速任务,有些用于长时间执行。

【讨论】:

因为在幕后,请求会启动单独的线程来完成更持久的工作,如果我关闭第一个连接,我将无法轻松地再次与这些线程交谈。我现在所拥有的是向数据库写入内容的线程和 ajax 调用每隔几秒就拉出农场中的任何一个 Web 服务器,它们读取数据库并查看是否有任何完成。通过使用一个表示打开的 Web 套接字,我将其锁定到一台服务器(一段时间),并让线程能够直接响应,而无需涉及数据库。

以上是关于在没有弹簧身份验证的情况下通过 web-socket 多次回复的主要内容,如果未能解决你的问题,请参考以下文章

Laravel sanctum 检查用户是不是在没有重定向的情况下通过身份验证

如何从控制器调用身份验证 - 弹簧安全核心插件

弹簧安全过滤器没有启动

无法使用弹簧自定义过滤器对用户进行身份验证

仅在需要身份验证时应用弹簧安全过滤器(由我的应用程序)

在没有 IIS 管理器的情况下禁用基本身份验证