You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

开启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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.08 05:13:13