Spring Boot与STOMP下WebSocket的CSRF验证问题咨询
你遇到的问题是Spring Security默认的WebSocket CSRF验证流程依赖HTTP握手阶段传递Token,但你的场景无法在握手时携带自定义头,且GET请求默认不触发CSRF校验,导致后续CsrfChannelInterceptor无法验证STOMP头中的Token。以下是几种可行的实现方案:
方案一:强制握手GET请求校验CSRF,通过Cookie传递Token
Spring Security默认不对GET请求做CSRF校验,但可以针对WebSocket握手路径强制开启,同时用Cookie传递Token(Spring Security推荐的方式),让CsrfTokenHandshakeInterceptor自动读取:
1. Spring Security配置
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf // 配置Cookie存储CSRF Token,允许前端读取 .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) // 仅对WebSocket握手路径(假设为/ws)强制CSRF校验 .requireCsrfProtectionMatcher(request -> "/ws".equals(request.getRequestURI()) && "GET".equals(request.getMethod()) ) ) .authorizeRequests(auth -> auth .antMatchers("/ws").authenticated() .anyRequest().permitAll() ); } }
2. WebSocket配置
无需额外修改默认拦截器,CsrfTokenHandshakeInterceptor会自动从Cookie中读取XSRF-TOKEN,并存入WebSocket会话,后续CsrfChannelInterceptor会验证STOMP头中的X-XSRF-TOKEN是否匹配。
前端处理
前端从Cookie中读取XSRF-TOKEN,在STOMP连接时放入头中:
const token = document.cookie.split('; ').find(row => row.startsWith('XSRF-TOKEN=')).split('=')[1]; const stompClient = new StompJs.Client({ brokerURL: 'ws://localhost:8080/ws', connectHeaders: { 'X-XSRF-TOKEN': token } });
方案二:自定义拦截器,直接验证STOMP头中的Token
如果无法在握手阶段传递Token,可以跳过握手时的Token存储,直接在STOMP消息阶段验证Token与后端存储的一致性:
自定义CsrfChannelInterceptor
public class CustomStompCsrfInterceptor extends CsrfChannelInterceptor { private final CsrfTokenRepository tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse(); @Override public Message<?> preSend(Message<?> message, MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); // 仅在CONNECT阶段验证Token if (StompCommand.CONNECT.equals(accessor.getCommand())) { // 从STOMP头获取Token String submittedToken = accessor.getFirstNativeHeader("X-XSRF-TOKEN"); // 从当前请求或会话获取预期的Token HttpServletRequest request = (HttpServletRequest) accessor.getSessionAttributes().get("javax.servlet.http.request"); CsrfToken expectedToken = tokenRepository.loadToken(request); if (expectedToken == null || !expectedToken.getToken().equals(submittedToken)) { throw new CsrfException("Invalid CSRF Token"); } } return super.preSend(message, channel); } }
配置WebSocket拦截器
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureClientInboundChannel(ChannelRegistration registration) { // 替换默认拦截器为自定义实现 registration.interceptors(new CustomStompCsrfInterceptor()); } // 其他WebSocket配置(如代理、前缀等)... }
方案三:通过查询参数传递Token并让握手拦截器识别
如果必须用查询参数传递,需要确保Spring Security对握手GET请求开启CSRF校验,让CsrfTokenHandshakeInterceptor读取_csrf参数:
Security配置调整
http.csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .requireCsrfProtectionMatcher(request -> "/ws".equals(request.getRequestURI()) && "GET".equals(request.getMethod()) ) );
前端握手URL
将Token作为_csrf参数拼接到握手URL中:
const token = document.cookie.split('; ').find(row => row.startsWith('XSRF-TOKEN=')).split('=')[1]; const stompClient = new StompJs.Client({ brokerURL: `ws://localhost:8080/ws?_csrf=${token}` });
此时CsrfTokenHandshakeInterceptor会读取查询参数中的Token并存入会话,后续STOMP消息的头验证流程正常工作。
内容的提问来源于stack exchange,提问作者Olli

