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

如何通过Spring Cloud Gateway保障微服务WebSocket端点安全?

微服务架构下WebSocket的CSRF校验问题

背景

基于微服务架构,前端部署Spring Cloud Gateway,安全控制集中在网关层:

  • 已配置OIDC+OAuth2实现认证授权,网关同时作为OAuth客户端和资源服务器,采用授权码流程
  • 通过TokenRelay过滤器将认证信息传递给下游服务,已启用CORS和默认CSRF防护

核心问题

常规HTTP请求正常工作:下游服务通过TokenRelay添加的Authorization header认证,并跳过CSRF校验(信任网关)。但WebSocket连接无法复用该逻辑:

  • 启用@EnableWebsocketSecurity时,强制CSRF校验,但传递网关生成的token会触发InvalidCsrfTokenException(下游服务有独立token仓库)
  • 禁用WebSocket安全时,隐式关闭CSRF,网关token不影响连接

期望:让WebSocket实现与HTTP请求一致的安全逻辑,不想搭建繁琐的集中式token仓库。当前临时方案是禁用微服务的CSRF,依赖Spring Security/Spring Websockets的服务器端Origin头校验(同源策略)。

补充疑问:HTTP请求中存在Bearer token时,CsrfFilter会跳过校验(因微服务是OAuth2资源服务器),但WebSocket无此逻辑,这是设计如此还是Bug?

相关代码

下游服务配置

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@EnableWebSocketSecurity
public class DownstreamServiceSecurityConfiguration {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
                .csrf(csrfSpec -> csrfSpec.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
                .oauth2ResourceServer((oauth2) -> oauth2.jwt(withDefaults()));
        return http.build();
    }

.....

网关配置

@Configuration
@EnableWebFluxSecurity
public class GatewaySecurityConfiguration {

    @Autowired
    public SecurityConfiguration(ReactiveClientRegistrationRepository clientRegistrationRepository) {
        this.clientRegistrationRepository = clientRegistrationRepository;
    }

    ReactiveClientRegistrationRepository clientRegistrationRepository;

    private ServerLogoutSuccessHandler serverLogoutSuccessHandler() {
        OidcClientInitiatedServerLogoutSuccessHandler successHandler = new OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository);
        //needs to match the one declared at auth-server
        successHandler.setPostLogoutRedirectUri("{baseUrl}/api-docs");
        return successHandler;
    }

    @Bean
    public SecurityWebFilterChain filterChain(ServerHttpSecurity http){

        http.cors(withDefaults())
                // add csrf token that can be handled by js clients by using CookieServerCsrfTokenRepository.withHttpOnlyFalse()
                .csrf(csrfSpec -> csrfSpec.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())
                        .csrfTokenRequestHandler(new SpaServerCsrfTokenRequestHandler()))
                .authorizeExchange(authorizeExchangeSpec -> authorizeExchangeSpec.pathMatchers("/api-docs/**", "/swagger-ui.html", "/webjars/swagger-ui/**", "/actuator/**", "/oidc/**").permitAll()
                        .pathMatchers("/notification/ws-connect").hasAuthority("SCOPE_customer-read")
                        .anyExchange().authenticated())
                //handle oidc authentication by redirecting to auth server login page
                .oauth2Login(withDefaults())
                //make gateway also a resource server that supports jwt bearer token, as the default configuration does not kick in because we also have the client dependency on the classpath
                .oauth2ResourceServer(oAuth2ResourceServerSpec -> oAuth2ResourceServerSpec.jwt(withDefaults()))
                .logout(httpSecurityLogoutConfigurer -> httpSecurityLogoutConfigurer.logoutSuccessHandler(serverLogoutSuccessHandler()));
        return http.build();
    }

    //When storing the expected CSRF token in a cookie, JavaScript applications will only have access to the plain token value and will not have access to the encoded value.
    //A customized request handler for resolving the actual token value will need to be provided.
    static final class SpaServerCsrfTokenRequestHandler extends ServerCsrfTokenRequestAttributeHandler {
        private final ServerCsrfTokenRequestAttributeHandler delegate = new XorServerCsrfTokenRequestAttributeHandler();

        @Override
        public void handle(ServerWebExchange exchange, Mono<CsrfToken> csrfToken) {
             // Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of the CsrfToken when it is rendered in the response body.
            this.delegate.handle(exchange, csrfToken);
        }

        @Override
        public Mono<String> resolveCsrfTokenValue(ServerWebExchange exchange, CsrfToken csrfToken) {
            final var hasHeader = exchange.getRequest().getHeaders().get(csrfToken.getHeaderName()) !=null;
            return hasHeader ? super.resolveCsrfTokenValue(exchange, csrfToken) : this.delegate.resolveCsrfTokenValue(exchange, csrfToken);
        }
    }

    @Bean
    //Needed in order to set the XSRF-TOKEN cookie
   public WebFilter csrfCookieWebFilter() {
        return (exchange, chain) -> {
            exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty()).subscribe(o -> ((CsrfToken)o).getToken());
            return chain.filter(exchange);
        };
    }


    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(List.of("http://localhost:63342"));
        configuration.setAllowedMethods(List.of(CorsConfiguration.ALL));
        configuration.setAllowCredentials(true);
        configuration.setAllowedHeaders(List.of(CorsConfiguration.ALL));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

客户端代码

const stompClient = new StompJs.Client({
            brokerURL: 'ws://localhost:9990/notification/ws-connect',
            connectHeaders : {"X-XSRF-TOKEN":csrfToken}
        });

错误信息

org.springframework.security.web.csrf.InvalidCsrfTokenException: Invalid CSRF Token 'd1f38e4e-71cc-4241-8c7e-3b01c1937c7c' was found on the request parameter '_csrf' or header 'X-XSRF-TOKEN'

架构示意图

架构示意图

解决方案建议

方案1:下游服务WebSocket安全中跳过CSRF校验(信任网关)

既然HTTP请求中已通过TokenRelay传递JWT,且下游服务信任网关的认证,那么可以对WebSocket请求做同样的信任处理:
在下游服务的WebSocket安全配置中,添加自定义CSRF校验逻辑,当请求带有网关转发的JWT时,跳过CSRF校验。

修改下游服务配置:

@Configuration
@EnableWebSocketSecurity
public class WebSocketSecurityConfig {

    @Bean
    public WebSocketSecurityFilterChain webSocketSecurityFilterChain(WebSocketSecurity webSocket) throws Exception {
        webSocket.authorizeMessages(messages -> messages.anyMessage().authenticated())
                .csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                        // 自定义CSRF校验器,当存在Authorization头时跳过校验
                        .csrfTokenValidator(new CsrfTokenValidator() {
                            @Override
                            public void validate(HttpServletRequest request, CsrfToken csrfToken) {
                                String authHeader = request.getHeader("Authorization");
                                if (authHeader != null && authHeader.startsWith("Bearer ")) {
                                    // 存在JWT,跳过CSRF校验
                                    return;
                                }
                                // 否则执行默认校验
                                new DefaultCsrfTokenValidator().validate(request, csrfToken);
                            }
                        }));
        return webSocket.build();
    }
}

方案2:网关统一处理WebSocket的CSRF,下游服务禁用WebSocket CSRF

保持网关的CSRF校验,下游服务对WebSocket请求关闭CSRF校验,同时依赖Origin头校验和JWT认证保障安全:

  1. 下游服务移除@EnableWebSocketSecurity,或在WebSocket安全配置中禁用CSRF:
@Configuration
@EnableWebSocketSecurity
public class WebSocketSecurityConfig {

    @Bean
    public WebSocketSecurityFilterChain webSocketSecurityFilterChain(WebSocketSecurity webSocket) throws Exception {
        webSocket.authorizeMessages(messages -> messages.anyMessage().authenticated())
                .csrf(csrf -> csrf.disable());
        return webSocket.build();
    }
}
  1. 确保网关已对WebSocket路径做权限校验(如网关配置中的pathMatchers("/notification/ws-connect").hasAuthority("SCOPE_customer-read")),且下游服务仅接受来自网关的请求(可通过网络策略或IP白名单加固)。

关于补充疑问的解答

这是设计如此:Spring Security中,HTTP的CsrfFilter会检测请求是否携带Bearer Token(OAuth2资源服务器场景),并自动跳过CSRF校验,因为JWT本身已具备抗CSRF的能力(无法通过Cookie自动携带)。但WebSocket的安全逻辑是独立实现的,目前没有内置的类似跳过逻辑,需要手动配置。


内容的提问来源于stack exchange,提问作者IonutB

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 23:35:03