开启WebSocket安全时Spring STOMP无法从连接头解析会话令牌
问题分析与解决方案
核心问题
你遇到的是**@EnableWebSocketSecurity与HeaderHttpSessionIdResolver的兼容性冲突**,根源在于:
- 启用WebSocket Security后,Spring自动注入的
XorCsrfChannelInterceptor默认依赖Cookie关联HTTP会话,而你用Header传递会话ID时,WebSocket会话无法正确绑定到对应的HTTP会话,导致CSRF令牌验证失败。 - WebSocket消息通道默认不会自动触发
HeaderHttpSessionIdResolver解析请求头里的会话ID,使得SecurityContext中没有正确的用户/会话信息,进一步触发权限拒绝。
解决方案
步骤1:自定义WebSocket Security配置,适配Header会话场景
替换默认CSRF拦截逻辑,让它从STOMP请求头读取CSRF令牌,并关联Header传递的会话ID:
@Configuration @EnableWebSocketSecurity public class WebSocketSecurityConfig { private final HttpSessionIdResolver sessionIdResolver; public WebSocketSecurityConfig(HttpSessionIdResolver sessionIdResolver) { this.sessionIdResolver = sessionIdResolver; } @Bean public AuthorizationManager<Message<?>> authorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) { messages // 先放行CONNECT请求,确保会话绑定逻辑能执行 .simpMessageTypeMatchers(SimpMessageType.CONNECT).permitAll() .simpMessageDestMatchers("/queue/**", "/topic/**").denyAll() .simpSubscribeDestMatchers("/queue/*-user*", "/topic/*-user*").denyAll() .anyMessage().permitAll(); return messages.build(); } // 自定义CSRF拦截器,适配Header会话ID场景 @Bean public ChannelInterceptor csrfChannelInterceptor() { XorCsrfChannelInterceptor interceptor = new XorCsrfChannelInterceptor(); // 配置从STOMP原生头读取CSRF令牌 interceptor.setCsrfTokenRepository(new CsrfTokenRepository() { @Override public CsrfToken generateToken(HttpServletRequest request) { return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", UUID.randomUUID().toString()); } @Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { // 用Header传递令牌,无需保存到Cookie } @Override public CsrfToken loadToken(HttpServletRequest request) { return null; // 主要在拦截器逻辑中处理令牌匹配 } }); // 关联自定义的会话ID解析器,让CSRF拦截器能找到对应HTTP会话 interceptor.setSessionIdResolver(sessionIdResolver); return interceptor; } }
步骤2:添加会话绑定拦截器,优先解析Header会话ID
创建拦截器在STOMP CONNECT阶段解析X-Auth-Token,绑定对应的HTTP会话到SecurityContext:
@Component @Order(Ordered.HIGHEST_PRECEDENCE) // 确保拦截器最先执行,避免被其他拦截器拦截 public class SessionBindingChannelInterceptor implements ChannelInterceptor { private final HttpSessionIdResolver sessionIdResolver; private final FindByIndexNameSessionRepository<? extends Session> sessionRepository; public SessionBindingChannelInterceptor(HttpSessionIdResolver sessionIdResolver, FindByIndexNameSessionRepository<? extends Session> sessionRepository) { this.sessionIdResolver = sessionIdResolver; this.sessionRepository = sessionRepository; } @Override public Message<?> preSend(Message<?> message, MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); if (accessor != null && SimpMessageType.CONNECT.equals(accessor.getMessageType())) { // 从STOMP头中提取X-Auth-Token List<String> authTokenHeaders = accessor.getNativeHeader("X-Auth-Token"); if (authTokenHeaders != null && !authTokenHeaders.isEmpty()) { String sessionId = authTokenHeaders.get(0); // 从Spring Session仓库获取对应会话 Session session = sessionRepository.findById(sessionId); if (session != null) { // 将HTTP会话中的SecurityContext绑定到当前线程 SecurityContext securityContext = (SecurityContext) session.getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); if (securityContext != null) { SecurityContextHolder.setContext(securityContext); } } } } return message; } }
步骤3:同步HTTP Security配置逻辑
确保HTTP Security允许WebSocket握手请求,且CSRF令牌生成与传递逻辑一致:
@Configuration @EnableWebSecurity public class WebSecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // 若前端从Cookie取CSRF令牌则用这个,否则自定义Header仓库 ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .sessionIdResolver(httpSessionIdResolver()) ) .authorizeHttpRequests(auth -> auth .requestMatchers("/ws/**").permitAll() // 放行WebSocket握手路径 .anyRequest().authenticated() ); return http.build(); } @Bean public HttpSessionIdResolver httpSessionIdResolver() { return HeaderHttpSessionIdResolver.xAuthToken(); } }
关键说明
- 自定义拦截器之前只收到DISCONNECT请求,是因为默认WebSocket Security拦截器优先级更高,你的拦截器被排在后面,给自定义拦截器加上
@Order(Ordered.HIGHEST_PRECEDENCE)就能解决。 - CSRF验证失败的本质是WebSocket会话未关联到正确的HTTP会话,导致令牌不匹配,通过给CSRF拦截器关联SessionIdResolver就能解决。
- 测试时要确保前端发送的
X-Auth-Token和X-CSRF-TOKEN与HTTP会话中的值完全对应。
内容的提问来源于stack exchange,提问作者zeroonzeroone
相关产品推荐
相关产品推荐

