Disconnect client session from Spring websocket stomp server
Asked Answered
C

7

36

I've searched quite a bit and been unable to find this: Is there a way that a spring websocket stomp server can disconnect a client based on the sessionId (or really based on anything at all)?

It seems to me that once a client connects to a server there is nothing that allows the server to disconnect the client.

Cranium answered 16/2, 2015 at 23:17 Comment(0)
R
4

As far as I know the API doesn't provide what you are looking for, on server-side you can only detect disconnect events. If you want to disconnect a certain client I think you must go for a litte workaround, e.g. this one:

  1. Write a client-side javascript function that is able to trigger a disconnect
  2. As soon as your client is connected to the server, generate a client ID in your javascript and send it to the server. Remember the ID on the client, you'll need it in step (4).
  3. At the time you want the server to disconnect the connection to the specific client (identified by the ID), send a message containing the ID back to the client.
  4. Now your client javascript evaluates the message send from the server and decides to call the disconnect function you wrote in step (1).
  5. Your client disconnects itself.

The workaround is a bit cumbersome but it'll work.

Reichsmark answered 20/2, 2015 at 10:10 Comment(2)
Unfortunately that assumes the client complies with the disconnect message. Ultimately I don't want the client to have a say in it. Marking as the answer because it doesn't seem it is possible.Cranium
According to source code of SubProtocolWebSocketHandler.java's afterConnectionEstablished() method, you can read: // WebSocketHandlerDecorator could close the sessionRehm
R
21

Actually using some workarounds you can achieve what you want. For that you should do:

  1. Use java configuration (not sure if it is possible with XML config)
  2. Extend your config class from WebSocketMessageBrokerConfigurationSupport and implement WebSocketMessageBrokerConfigurer interface
  3. Create custom sub-protocol websocket handler and extend it from SubProtocolWebSocketHandler class
  4. In your custom sub-protocol websocket handler override afterConnectionEstablished method and you will have access to WebSocketSession :)

I've created sample spring-boot project to show how we can disconnect client session from server side: https://github.com/isaranchuk/spring-websocket-disconnect

Rata answered 28/8, 2015 at 11:38 Comment(5)
This response should be accepted, it works without client cooperationPolygamy
Is this still the only way to close the websocket?Pyrotechnics
This works. though I had to add spring.main.allow-bean-definition-overriding=true this as the bean for SubProtocolWebSocketHandler was already thereBiparty
@Biparty can you share how did you extend and create your subProtocolWebSocket class? I cannot find examples anywhere and I cannot get this to work.Pascale
where do you define clientInboundChannel() and clientOutBoundChannel()?Pascale
C
8

You can also disconnect session by implementing a custom 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();
    }
}
Camber answered 14/1, 2017 at 19:54 Comment(3)
Hi, thanks for the solution. Just wondering. What if the message is still not processed when the code hits afterConnectionEstablished. Is that scenario possible?Pyrotechnics
so I tried your code and the session is being closed even before the messages are processed.Pyrotechnics
@Pyrotechnics I guess this was just a behavior example to show that you can obtain the session and close it.Barret
M
5

I relied on the idea of @Dániel Kis and implemented the websocket session management with the key point of storing websocket sessions for authenticated users in Singleton-like object.

// 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);
                    }
                };
            }
        });
    }
}

Class to store websocket users' sessions WebsocketSessionHolder. I use 'synchronized' blocks for thread safety. Actually this blocks are not expensive operations because each of methods (addSession and closeSessions) are used not so often (On establishing and terminating connection). No need to use ConcurrentHashMap or SynchronizedMap here because we perform bunch of operations with the list in these methods.

// 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);
            }
        }
    }
}

And the final touch - terminating (disconnecting) specified user websocket sessions ("ADMIN" in the example), say in some Controller

//PageController.java

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

        WebsocketSessionHolder.closeSessions("ADMIN");
    }
}
Manouch answered 30/6, 2020 at 10:22 Comment(2)
There is a drawback to this solution that all sessions for the same user will be closed in case there are more than one session established.Piton
@BuSaeed you can use the session id as a connection mark, then store the session id as the key, store the websocketSession as a value, close the websocketSession based the session idMurrhine
R
4

As far as I know the API doesn't provide what you are looking for, on server-side you can only detect disconnect events. If you want to disconnect a certain client I think you must go for a litte workaround, e.g. this one:

  1. Write a client-side javascript function that is able to trigger a disconnect
  2. As soon as your client is connected to the server, generate a client ID in your javascript and send it to the server. Remember the ID on the client, you'll need it in step (4).
  3. At the time you want the server to disconnect the connection to the specific client (identified by the ID), send a message containing the ID back to the client.
  4. Now your client javascript evaluates the message send from the server and decides to call the disconnect function you wrote in step (1).
  5. Your client disconnects itself.

The workaround is a bit cumbersome but it'll work.

Reichsmark answered 20/2, 2015 at 10:10 Comment(2)
Unfortunately that assumes the client complies with the disconnect message. Ultimately I don't want the client to have a say in it. Marking as the answer because it doesn't seem it is possible.Cranium
According to source code of SubProtocolWebSocketHandler.java's afterConnectionEstablished() method, you can read: // WebSocketHandlerDecorator could close the sessionRehm
H
0

In case of xml configuration you can use <websocket:decorator-factories> in the <websocket:transport> of your <websocket:message-broker>. Create custom WebSocketHandlerDecorator and WebSocketHandlerDecoratorFactory which implement decorate method.

Harbor answered 10/5, 2018 at 14:32 Comment(0)
D
0

This may seem brief but I am not certain what the implementation would look like in your case. But, I think there are some circumstances that would warrant this workaround/solution:

  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. If you want to keep the session open - continue to send messages or else actively send ping/pongs. In the case that you want the session to disconnect, stop the ping/pong interaction somewhere suitable in you application.

Of course, if you are wanting to disconnect immediately, this doesn't seem to be an appropriate solution. But if you are simply trying to reduce the number of active connections, ping/pong may be a good fit since it keeps a session open only so long as messages are actively being sent, preventing the session from being closed prematurely.

Distributary answered 1/1, 2020 at 17:3 Comment(0)
I
0

first you have to introduce a class as your User class by inheritance then use it like this:

if (userObject instanceof User) {
    User user = (User) userObject;
    if (user.getId().equals(userDTO.getId())) {
       for (SessionInformation information : sessionRegistry.getAllSessions(user, true)) {
          information.expireNow();
       }
    }
}    
Issus answered 17/3, 2020 at 17:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.