35

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.

user1751547
  • 2,211
  • 4
  • 21
  • 25

7 Answers7

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

isaranchuk
  • 362
  • 3
  • 13
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();
    }
}
Dániel Kis
  • 2,341
  • 5
  • 28
  • 51
  • Hi, thanks for the solution. Just wondering. What if the message is still not processed when the code hits `afterConnectionEstablished`. Is that scenario possible? – leventunver Aug 20 '18 at 07:30
  • so I tried your code and the session is being closed even before the messages are processed. – leventunver Aug 20 '18 at 08:05
  • @leventunver I guess this was just a behavior example to show that you can obtain the session and close it. – Inego Feb 22 '22 at 10:24
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");
    }
}
  • 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. – Bu Saeed Apr 15 '23 at 20:00
3

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.

mika
  • 2,495
  • 22
  • 30
  • 9
    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. – user1751547 Feb 20 '15 at 18:39
  • 2
    According to source code of `SubProtocolWebSocketHandler.java`'s `afterConnectionEstablished()` method, you can read: `// WebSocketHandlerDecorator could close the session` – Dániel Kis Jan 14 '17 at 20:03
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.

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.

Christian Meyer
  • 605
  • 8
  • 15
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();
       }
    }
}    
Charlie873
  • 11
  • 5