Отключить сеанс клиента от сервера Spring WebSocket Stomp

Я довольно много искал и не смог найти это: есть ли способ, которым сервер Spring WebSocket Stomp может отключить клиента на основе sessionId (или действительно на основе чего-либо вообще)?

Мне кажется, что как только клиент подключается к серверу, нет ничего, что позволяло бы серверу отключить клиента.


person user1751547    schedule 16.02.2015    source источник


Ответы (7)


Насколько мне известно, API не предоставляет то, что вы ищете, на стороне сервера вы можете обнаруживать только события отключения. Если вы хотите отключить определенного клиента, я думаю, вам нужно использовать небольшое обходное решение, например Вот этот:

  1. Напишите клиентскую функцию javascript, которая может вызвать отключение
  2. Как только ваш клиент подключится к серверу, сгенерируйте идентификатор клиента в своем javascript и отправьте его на сервер. Запомните ID на клиенте, он вам понадобится на шаге (4).
  3. В тот момент, когда вы хотите, чтобы сервер отключил соединение с конкретным клиентом (идентифицированным идентификатором), отправьте сообщение, содержащее идентификатор, обратно клиенту.
  4. Теперь ваш клиентский javascript оценивает сообщение, отправленное с сервера, и решает вызвать функцию отключения, которую вы написали на шаге (1).
  5. Ваш клиент отключается.

Обходной путь немного громоздкий, но он сработает.

person mika    schedule 20.02.2015
comment
К сожалению, это предполагает, что клиент соответствует сообщению об отключении. В конечном итоге я не хочу, чтобы клиент имел право голоса в этом вопросе. Помечено как ответ, потому что это не кажется возможным. - person user1751547; 20.02.2015
comment
Согласно исходному коду метода SubProtocolWebSocketHandler.java afterConnectionEstablished() вы можете прочитать: // WebSocketHandlerDecorator could close the session - person Dániel Kis; 14.01.2017

На самом деле, используя некоторые обходные пути, вы можете добиться того, чего хотите. Для этого вам необходимо сделать:

  1. Используйте конфигурацию java (не уверен, возможно ли это с конфигурацией XML)
  2. Расширьте свой класс конфигурации из WebSocketMessageBrokerConfigurationSupport и реализуйте интерфейс WebSocketMessageBrokerConfigurer
  3. Создайте собственный обработчик веб-сокетов подпротокола и расширьте его из класса SubProtocolWebSocketHandler
  4. В вашем пользовательском суб-протоколе обработчик веб-сокетов переопределите метод afterConnectionEstablished, и у вас будет доступ к WebSocketSession :)

Я создал образец проекта весенней загрузки, чтобы показать, как мы можем отключить сеанс клиента от сервера: https://github.com/isaranchuk/spring-websocket-disconnect

person isaranchuk    schedule 28.08.2015
comment
Этот ответ следует принять, он работает без сотрудничества с клиентом. - person Alessandro Polverini; 20.10.2015
comment
Это все еще единственный способ закрыть веб-узел? - person leventunver; 26.08.2018
comment
Это работает. хотя мне пришлось добавить spring.main.allow-bean-definition-overriding = true, поскольку bean для SubProtocolWebSocketHandler уже был там - person Vikash; 27.10.2020

Вы также можете отключить сеанс, реализовав собственный WebSocketHandlerDecorator:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig<S extends ExpiringSession> extends AbstractSessionWebSocketMessageBrokerConfigurer<S> {

    @Override
    public void configureWebSocketTransport(final WebSocketTransportRegistration registration) {
        registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() {
            @Override
            public WebSocketHandler decorate(final WebSocketHandler handler) {
                return new WebSocketHandlerDecorator(handler) {
                    @Override
                    public void afterConnectionEstablished(final WebSocketSession session) throws Exception {

                        session.close(CloseStatus.NOT_ACCEPTABLE);
                        super.afterConnectionEstablished(session);
                    }
                };
            }
        });
        super.configureWebSocketTransport(registration);
    }


    @Override
    protected void configureStompEndpoints(final StompEndpointRegistry registry) {
    registry.addEndpoint("/home")
            .setHandshakeHandler(new DefaultHandshakeHandler(
                    new UndertowRequestUpgradeStrategy() // If you use undertow
                    // new JettyRequestUpgradeStrategy()
                    // new TomcatRequestUpgradeStrategy()
            ))
            .withSockJS();
    }
}
person Dániel Kis    schedule 14.01.2017
comment
Привет, спасибо за решение. Просто интересуюсь. Что делать, если сообщение все еще не обрабатывается, когда код достигает afterConnectionEstablished. Возможен ли такой сценарий? - person leventunver; 20.08.2018
comment
поэтому я попробовал ваш код, и сеанс закрывается еще до обработки сообщений. - person leventunver; 20.08.2018

сначала вы должны ввести класс в качестве своего класса пользователя по наследству, а затем использовать его следующим образом:

if (userObject instanceof User) {
    User user = (User) userObject;
    if (user.getId().equals(userDTO.getId())) {
       for (SessionInformation information : sessionRegistry.getAllSessions(user, true)) {
          information.expireNow();
       }
    }
}    
person Charlie873    schedule 17.03.2020

Я опирался на идею @ Dániel Kis и реализовал управление сеансами веб-сокетов с ключевой точкой хранения сеансов веб-сокетов для аутентифицированных пользователей в объекте типа Singleton.

// WebSocketConfig.java

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
        registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() {
            @Override
            public WebSocketHandler decorate(final WebSocketHandler handler) {
                return new WebSocketHandlerDecorator(handler) {

                    @Override
                    public void afterConnectionEstablished(final WebSocketSession session) throws Exception {

                        // We will store current user's session into WebsocketSessionHolder after connection is established
                        String username = session.getPrincipal().getName();
                        WebsocketSessionHolder.addSession(username, session);

                        super.afterConnectionEstablished(session);
                    }
                };
            }
        });
    }
}

Класс для хранения сеансов пользователей веб-сокетов WebsocketSessionHolder. Я использую «синхронизированные» блоки для обеспечения безопасности потоков. На самом деле эти блоки не являются дорогостоящими операциями, потому что каждый из методов (addSession и closeSessions) используется не так часто (при установлении и завершении соединения). Здесь нет необходимости использовать ConcurrentHashMap или SynchronizedMap, потому что мы выполняем множество операций со списком в этих методах.

// WebsocketSessionHolder.java

public class WebsocketSessionHolder {

    static {
        sessions = new HashMap<>();
    }
    
    // key - username, value - List of user's sessions
    private static Map<String, List<WebSocketSession>> sessions;

    public static void addSession(String username, WebSocketSession session)
    {
        synchronized (sessions) {
            var userSessions = sessions.get(username);
            if (userSessions == null)
                userSessions = new ArrayList<WebSocketSession>();

            userSessions.add(session);
            sessions.put(username, userSessions);
        }
    }

    public static void closeSessions(String username) throws IOException 
    {
        synchronized (sessions) {
            var userSessions = sessions.get(username);
            if (userSessions != null)
            {
                for(var session : userSessions) {
                    // I use POLICY_VIOLATION to indicate reason of disconnecting for a client
                    session.close(CloseStatus.POLICY_VIOLATION);
                }
                sessions.remove(username);
            }
        }
    }
}

И последний штрих - завершение (отключение) определенных пользовательских сеансов веб-сокетов (ADMIN в примере), скажем, в каком-то контроллере

//PageController.java

@Controller
public class PageController {
    @GetMapping("/kill-sessions")
    public void killSessions() throws Exception {

        WebsocketSessionHolder.closeSessions("ADMIN");
    }
}
person Rustam Shafigullin    schedule 30.06.2020

В случае конфигурации xml вы можете использовать <websocket:decorator-factories> в <websocket:transport> вашего <websocket:message-broker>. Создайте пользовательские WebSocketHandlerDecorator и WebSocketHandlerDecoratorFactory, реализующие метод decorate.

person Karlovskiy Alexey    schedule 10.05.2018

Это может показаться кратким, но я не уверен, как будет выглядеть реализация в вашем случае. Но я думаю, что есть некоторые обстоятельства, которые требуют этого обходного пути / решения:

  1. Set a timeout on the back-end (say 30 seconds):
    • This is how you would do it with Spring Boot Websocket (and Tomcat):
    @Bean
    public ServletServerContainerFactoryBean websocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        container.setMaxSessionIdleTimeout(MAX_SESSION_IDLE_TIMEOUT); 
        return container;
    }
  1. Если вы хотите, чтобы сеанс оставался открытым - продолжайте отправлять сообщения или активно отправляйте пинг / понги. В случае, если вы хотите, чтобы сеанс отключился, остановите взаимодействие пинг / понг в подходящем для вас приложении.

Конечно, если вы хотите немедленно отключиться, это не кажется подходящим решением. Но если вы просто пытаетесь уменьшить количество активных подключений, пинг / понг может оказаться подходящим вариантом, поскольку он поддерживает сеанс открытым только до тех пор, пока активно отправляются сообщения, предотвращая преждевременное закрытие сеанса.

person Christian Meyer    schedule 01.01.2020