SimpUserRegistry在EC2/Heroku异常:部分用户未加入注册表
根据你描述的场景——本地运行正常,但部署到AWS EC2(仅弹性IP无负载均衡)和Heroku时部分用户无法被SimpUserRegistry统计,结合排查到的现象(SockJS CONNECTED响应无user-name头、SessionConnectedEvent用户为null但SessionConnectEvent有正确用户),我来梳理下可能的根因和对应的解决办法:
可能的根因分析
WebSocket会话用户绑定时机不匹配
你提到SecurityChannelInterceptor完成了所有用户认证,但SessionConnectedEvent中的用户为null。这说明在SessionConnectedEvent触发时,用户信息还没有被正确关联到WebSocket会话上。SessionConnectEvent是在握手阶段触发的,此时拦截器已经设置了用户,但SessionConnectedEvent是在会话完全建立后触发,中间可能存在用户信息没有同步到会话上下文的情况。部署环境的WebSocket头信息传递问题
- Heroku环境本身会通过反向代理处理WebSocket请求,可能默认没有传递某些关键头(比如用户认证相关的头,或者WebSocket协议所需的头),导致会话建立后无法正确提取用户信息。
- AWS EC2虽然直接暴露,但如果你的应用使用了嵌入式容器(比如Tomcat),容器在公网环境下的WebSocket配置可能存在差异,导致
user-name头无法被正确添加到CONNECTED响应中。
SimpUserRegistry的实例一致性问题
如果你在多个地方注入SimpUserRegistry,有可能出现实例不一致的情况(比如某些地方注入的是代理实例,而非真正的注册表实例),导致统计时读取的是未更新的实例数据。
针对性解决方案
1. 确保用户信息在握手阶段就绑定到会话
放弃仅依赖通道拦截器设置用户,改用WebSocket握手拦截器在握手阶段就将用户信息绑定到会话属性中,这样SessionConnectedEvent触发时就能正确获取用户:
@Component public class HandshakeAuthInterceptor implements HandshakeInterceptor { private final SecurityService securityService; public HandshakeAuthInterceptor(SecurityService securityService) { this.securityService = securityService; } @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { // 替换成你实际的认证逻辑,从请求中获取认证信息 Authentication auth = securityService.getAuthenticationFromRequest(request); if (auth != null && auth.isAuthenticated()) { // 将用户信息存入会话属性,Spring WebSocket会自动关联到SimpUserRegistry attributes.put(Principal.class.getName(), auth); } return true; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {} }
然后在WebSocketMessageBrokerConfig中注册这个拦截器:
@Configuration @EnableWebSocketMessageBroker public class WebSocketMessageBrokerConfig implements WebSocketMessageBrokerConfigurer { private final HandshakeAuthInterceptor handshakeAuthInterceptor; public WebSocketMessageBrokerConfig(HandshakeAuthInterceptor handshakeAuthInterceptor) { this.handshakeAuthInterceptor = handshakeAuthInterceptor; } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws") .withSockJS() .setInterceptors(handshakeAuthInterceptor); // 添加握手拦截器 } // 其他配置... }
2. 手动处理SessionConnectEvent将用户注册到SimpUserRegistry
既然SessionConnectEvent中已经有正确的用户对象,我们可以监听这个事件,手动将用户注册到注册表中,避免依赖自动关联:
@Component public class SessionConnectEventListener { private final SimpUserRegistry simpUserRegistry; public SessionConnectEventListener(SimpUserRegistry simpUserRegistry) { this.simpUserRegistry = simpUserRegistry; } @EventListener public void handleSessionConnect(SessionConnectEvent event) { Authentication auth = (Authentication) event.getUser(); if (auth != null && auth.isAuthenticated()) { // 手动创建SimpUser并注册 SimpUser simpUser = new DefaultSimpUser(auth.getName(), auth); // 注意:Spring 5+可使用SimpUserRegistry的registerUser方法,若版本不支持可向下转型为DefaultSimpUserRegistry ((DefaultSimpUserRegistry) simpUserRegistry).registerUser(simpUser); } } }
提示:若担心依赖内部实现类
DefaultSimpUserRegistry,也可以通过SimpMessagingTemplate获取会话上下文来关联用户。
3. 适配部署环境的WebSocket配置
- Heroku环境:确保应用已启用WebSocket支持,在
application.properties中添加:
spring.web.socket.enabled=true server.tomcat.additional-tld-skip-patterns=*.jar
同时配置信任Heroku反向代理传递的头信息,确保WebSocket握手请求的原始信息被正确解析:
@Configuration public class ServerConfig { @Bean public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCustomizer() { return factory -> factory.addConnectorCustomizers(connector -> { connector.setScheme("https"); connector.setProxyPort(443); }); } }
- AWS EC2环境:如果使用嵌入式Tomcat,检查Tomcat的WebSocket配置,比如设置合理的最大会话数、允许的Origin列表等,避免会话建立时出现异常导致用户无法注册。
4. 验证SimpUserRegistry的实例一致性
在所有使用SimpUserRegistry的地方(监听器、控制器、定时任务),打印实例的hashCode(),确认是否为同一个实例。如果出现多个实例,检查配置类是否存在重复创建实例的情况,比如是否同时使用了@Bean和@Autowired导致多实例。
额外调试建议
- 在
SecurityChannelInterceptor中添加日志,打印每个请求的认证结果和用户信息,确认认证确实完成。 - 在
SessionConnectedEvent监听器中打印会话ID和上下文信息,检查是否能获取到用户相关的属性。 - 查看浏览器的Network面板,检查WebSocket握手请求的响应头,确认是否有
user-name头,以及请求头中的认证信息是否正确传递。
内容的提问来源于stack exchange,提问作者Bhesh Gurung

