集成Spring Session至WebSocket实现Http Session清理及问题排查
Spring Boot WebSocket会话管理与认证问题排查
场景与需求
在Heroku部署Spring Boot WebSocket服务器,需实现以下功能:
- HTTP基础认证(基于PostgreSQL数据库)
- 单用户仅允许一个活跃会话
- 用户断开连接时自动清理/失效已认证会话,避免无法重连
现有Security配置
当前SecurityFilterChain配置如下:
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests((authz) -> authz.anyRequest().authenticated()) .httpBasic(withDefaults()) .sessionManagement(session -> session .maximumSessions(1) .maxSessionsPreventsLogin(true)); return http.build(); }
会话超时疑问
尝试设置server.servlet.session.timeout=1m,期望通过WebSocket心跳维持Http Session存活直至用户断开,但1分钟后连接仍断开。已知心跳对Heroku有效:设置server.servlet.session.timeout=10m后,客户端闲置数分钟未被Heroku(默认55秒切断闲置连接)断开。疑问:为何WebSocket心跳无法维持Http Session存活?
Spring Session配置错误及当前配置
为更好控制会话生命周期引入Spring Session,却遇到以下Bean缺失错误:
Field sessionRepository in org.springframework.session.web.socket.config.annotation.AbstractSessionWebSocketMessageBrokerConfigurer required a bean of type 'org.springframework.session.SessionRepository' that could not be found. The injection point has the following annotations: - @org.springframework.beans.factory.annotation.Autowired(required=true) Action: Consider defining a bean of type 'org.springframework.session.SessionRepository' in your configuration.
当前WebSocket配置类:
@Configuration @EnableWebSocketMessageBroker public class EngineConfig extends AbstractSessionWebSocketMessageBrokerConfigurer<Session> { @Override public void configureStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint(SRV_ENDPOINT); } @Override public void configureWebSocketTransport(WebSocketTransportRegistration registry) { registry.setMessageSizeLimit(MESSAGE_SIZE_LIMIT); registry.setSendBufferSizeLimit(SEND_BUFFER_SIZE_LIMIT); registry.setSendTimeLimit(SEND_TIME_LIMIT); } @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker(MSG_BROKER_PREFIX) .setHeartbeatValue(new long[]{HEART_BEAT_TIME, HEART_BEAT_SEND}) .setTaskScheduler(heartBeatScheduler()); config.setApplicationDestinationPrefixes(APP_DESTINATION_PREFIX); } @Bean public TaskScheduler heartBeatScheduler() { return new ConcurrentTaskScheduler(); } }
application.properties配置:
server.servlet.session.timeout=10m spring.datasource.url=jdbc:postgresql://localhost:5432/xxxx spring.datasource.username=xxxx spring.datasource.password=xxxx spring.datasource.driver-class-name=org.postgresql.Driver spring.session.store-type=jdbc spring.session.jdbc.initialize-schema=embedded spring.session.jdbc.schema=classpath:org/springframework/session/jdbc/schema-@@platform@@.sql spring.session.jdbc.table-name=SPRING_SESSION
解决建议
1. WebSocket心跳无法维持Http Session的原因与解决方案
- 核心差异:WebSocket心跳是WebSocket连接层面的保活机制,不会自动触发Http Session的活跃度刷新。Http Session的超时判定仅基于Http请求(如初始握手、后续Http接口调用),WebSocket帧不属于Http请求范畴,因此即使心跳持续发送,Http Session仍会按设置的超时时间失效。
- 修复方案:在WebSocket消息处理流程中手动刷新Http Session。例如通过拦截器实现:
@Component public class SessionRefreshInterceptor implements HandshakeInterceptor { @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { if (request instanceof ServletServerHttpRequest) { HttpSession session = ((ServletServerHttpRequest) request).getServletRequest().getSession(); session.setMaxInactiveInterval((int) TimeUnit.MINUTES.toSeconds(10)); // 重置超时时间 attributes.put("HTTP_SESSION_ID", session.getId()); // 存入会话属性用于后续清理 } return true; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { // 无需操作 } }
将拦截器添加到WebSocket端点:
@Override public void configureStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint(SRV_ENDPOINT).addInterceptors(new SessionRefreshInterceptor()); }
2. SessionRepository Bean缺失问题修复
- 检查依赖:确保Spring Boot项目引入了必要依赖(Maven示例):
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-core</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-jdbc</artifactId> </dependency>
- 替换旧版配置类:
AbstractSessionWebSocketMessageBrokerConfigurer是Spring Session旧版API,新版无需手动继承。简化WebSocket配置类:
@Configuration @EnableWebSocketMessageBroker public class EngineConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint(SRV_ENDPOINT).addInterceptors(new SessionRefreshInterceptor()); } @Override public void configureWebSocketTransport(WebSocketTransportRegistration registry) { registry.setMessageSizeLimit(MESSAGE_SIZE_LIMIT); registry.setSendBufferSizeLimit(SEND_BUFFER_SIZE_LIMIT); registry.setSendTimeLimit(SEND_TIME_LIMIT); } @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker(MSG_BROKER_PREFIX) .setHeartbeatValue(new long[]{HEART_BEAT_TIME, HEART_BEAT_SEND}) .setTaskScheduler(heartBeatScheduler()); config.setApplicationDestinationPrefixes(APP_DESTINATION_PREFIX); } @Bean public TaskScheduler heartBeatScheduler() { return new ConcurrentTaskScheduler(); } }
- 手动注册SessionRepository(可选):若Spring Boot自动配置未生效,手动创建Bean:
@Bean public SessionRepository<org.springframework.session.Session> sessionRepository(JdbcOperations jdbcOperations, PlatformTransactionManager transactionManager) { JdbcIndexedSessionRepository repository = new JdbcIndexedSessionRepository(jdbcOperations, transactionManager); repository.setTableName("SPRING_SESSION"); return repository; }
3. 断开连接时自动清理会话
实现WebSocket断开事件监听器,在连接关闭时主动失效会话:
@Component public class WebSocketSessionCleanupListener implements ApplicationListener<SessionDisconnectEvent> { private final SessionRepository<org.springframework.session.Session> sessionRepository; public WebSocketSessionCleanupListener(SessionRepository<org.springframework.session.Session> sessionRepository) { this.sessionRepository = sessionRepository; } @Override public void onApplicationEvent(SessionDisconnectEvent event) { String sessionId = event.getSessionAttributes().get("HTTP_SESSION_ID").toString(); org.springframework.session.Session session = sessionRepository.findById(sessionId); if (session != null) { sessionRepository.deleteById(sessionId); } } }
4. 协同Security会话管理与Spring Session
配置SessionRegistry让Security识别Spring Session存储的会话:
@Bean public SessionRegistry sessionRegistry(SessionRepository<org.springframework.session.Session> sessionRepository) { return new SpringSessionBackedSessionRegistry<>(sessionRepository); }
更新SecurityFilterChain配置:
@Bean public SecurityFilterChain filterChain(HttpSecurity http, SessionRegistry sessionRegistry) throws Exception { http.authorizeHttpRequests((authz) -> authz.anyRequest().authenticated()) .httpBasic(withDefaults()) .sessionManagement(session -> session .maximumSessions(1) .maxSessionsPreventsLogin(true) .sessionRegistry(sessionRegistry)); return http.build(); }
内容的提问来源于stack exchange,提问作者CT95
相关产品推荐
相关产品推荐

