How does Spring Session integrate with WebSocket connections, and why is that integration necessary?
answer
- WebSocket = handshake then no more HTTP requests
- Session would expire mid-conversation
- AbstractSessionWebSocketMessageBrokerConfigurer
- Interceptor touches lastAccessedTime
- WebSocketRegistryListener closes sockets on expiry event
basics
~20 sWebSocket connections are long-lived, so a session can expire while the socket stays open. Spring Session's WebSocket support keeps the session's last-accessed time updated on WebSocket activity and closes the socket when the session actually expires.
solid answer
~40 sA WebSocket is a long-lived, full-duplex connection opened after an initial HTTP handshake. The problem: after the handshake there are no more HTTP requests, so the associated HTTP session's `lastAccessedTime` never gets refreshed and it would expire mid-conversation — even though the user is active. Spring Session's WebSocket integration, enabled by extending **`AbstractSessionWebSocketMessageBrokerConfigurer`** (which registers a **`SessionRepositoryMessageInterceptor`** and a **`WebSocketRegistryListener`**), solves both directions: it **updates the session's last-accessed time** whenever WebSocket messages flow, keeping the session alive; and it **tracks which WebSocket sessions belong to which HTTP session** so that when the HTTP session is invalidated or expires (a `SessionDeletedEvent`/`SessionExpiredEvent` across the cluster), the corresponding WebSocket connections are **closed**. This keeps security/expiry consistent between HTTP and WebSocket in a clustered deployment. It's built on Spring's STOMP-over-WebSocket messaging.
code
java · 19 lines@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig
extends AbstractSessionWebSocketMessageBrokerConfigurer<Session> {
@Override
protected void configureStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic", "/queue");
registry.setApplicationDestinationPrefixes("/app");
}
// The base class auto-registers SessionRepositoryMessageInterceptor
// (keeps session alive on WS activity) and WebSocketRegistryListener
// (closes sockets when the HTTP session expires/is deleted).
}go deeper
Know WebSockets are long-lived and that Spring Session must keep the session alive and close sockets on expiry.
Name AbstractSessionWebSocketMessageBrokerConfigurer and the two-way behavior (keep-alive + close-on-expiry).
Explain the interceptor vs registry listener, dependence on session events, and STOMP scope.
Reason about clustered event delivery (indexed repo + keyspace notifications), security implications of revoking live channels, and handshake-time session binding.
## Background: what a WebSocket is A **WebSocket** is a persistent, bidirectional TCP connection established via an HTTP **Upgrade handshake**. After that single handshake, the client and server exchange frames indefinitely with **no further HTTP requests**. Spring's higher-level model puts **STOMP** (a simple text messaging protocol) on top, configured via `@EnableWebSocketMessageBroker`. ## The mismatch this integration fixes HTTP session expiry is driven by **`lastAccessedTime`**, refreshed on each HTTP request. But a WebSocket produces no HTTP requests after the handshake. So: 1. **Stale-expiry problem:** an actively-chatting user's HTTP session times out (default 30 min) because nothing is refreshing it. Their next HTTP request finds them logged out. 2. **Zombie-socket problem:** conversely, when the HTTP session *is* invalidated (logout) or expires, the WebSocket stays open — an authenticated live channel outliving the session that authorized it. A security and correctness hole, worse in a cluster where the expiry happens on a different node. ## How Spring Session integrates You extend **`AbstractSessionWebSocketMessageBrokerConfigurer`** instead of the plain `AbstractWebSocketMessageBrokerConfigurer`/implementing `WebSocketMessageBrokerConfigurer`. It wires up two collaborators: - **`SessionRepositoryMessageInterceptor`** — a `ChannelInterceptor` on the inbound WebSocket message channel. On each WebSocket message it **touches the associated Spring Session**, updating `lastAccessedTime` so activity on the socket keeps the HTTP session alive. (Solves problem 1.) - **`WebSocketRegistryListener`** — an `ApplicationListener` that maintains a mapping from HTTP session → set of open `WebSocketSession`s. It listens for Spring Session's **`SessionDeletedEvent`** and **`SessionExpiredEvent`**; when one fires for a session, it **closes all WebSocket connections** bound to it. (Solves problem 2.) Because these hang off Spring Session's *events*, they work across a cluster: node B can expire a session and, if the socket lives on node B, it's closed; the event-driven model is what makes clustered consistency possible (with the indexed repository firing events). ## Minimal setup ```java @Configuration @EnableWebSocketMessageBroker public class WsConfig extends AbstractSessionWebSocketMessageBrokerConfigurer<ExpiringSession> { @Override protected void configureStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic"); registry.setApplicationDestinationPrefixes("/app"); } } ``` (Note: the generic type parameter reflects older Spring Session versions; newer ones drop it.) ## Gotchas & edge cases - **Only STOMP-over-WebSocket** via the message-broker configurer is covered by this convenience base class. Raw `WebSocketHandler` usage needs manual wiring. - **Requires the events** — with the plain `RedisSessionRepository` (no events) the *close-on-expiry* behavior won't work; you need an **indexed** repository (and Redis keyspace notifications) so `SessionExpiredEvent` fires. - **The HTTP session must exist at handshake time** — the session ID from the handshake request is what binds the WebSocket to the session. If the handshake is anonymous, there's nothing to keep alive. - **Heartbeats:** STOMP heartbeats count as messages through the interceptor, so an idle-but-connected client can still keep the session alive — usually desirable, but worth knowing. - **Security:** this integration is *why* logout can forcibly tear down live sockets — important for compliance (revoking access must actually revoke the live channel). ## When to use Any app combining Spring Session clustering with STOMP WebSockets — dashboards, chat, notifications — where you need session lifetime and WebSocket lifetime to stay in lockstep.
- Why can't you just rely on the normal HTTP session timeout for a WebSocket client?After the handshake there are no HTTP requests to refresh lastAccessedTime, so an active WebSocket user's session would time out. The SessionRepositoryMessageInterceptor refreshes the session on WebSocket message activity to prevent this.
- What happens to open WebSockets when a user logs out in a clustered setup?Logout invalidates the HTTP session, publishing a SessionDeletedEvent. WebSocketRegistryListener catches it and closes all WebSocket connections bound to that session — so the live channel doesn't outlive authorization, even across nodes.
saying these in an interview costs you the question
- Claiming WebSocket traffic automatically refreshes the HTTP session without the interceptor
- Assuming sockets are torn down on session expiry without the integration
- Thinking it works with the non-indexed RedisSessionRepository that fires no events
- Confusing raw WebSocketHandler with the STOMP message-broker configurer this base class targets