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

如何在Spring API Gateway中拦截并处理STOMP请求?

Spring API Gateway 处理 STOMP 请求认证的实现方案

核心思路

STOMP 基于 WebSocket,分为HTTP 握手阶段和STOMP 帧传输阶段,网关需要分别处理这两个阶段的认证与用户信息传递:

  1. 握手阶段:利用 Gateway 的 OAuth2 能力完成认证,将用户ID注入握手请求头,同时把用户信息绑定到 WebSocket 会话。
  2. STOMP 帧阶段:拦截并修改 WebSocket 消息中的 STOMP 帧,从会话中取出用户ID注入 STOMP 头,确保后端能直接获取已认证的用户信息。

步骤1:配置 Gateway 处理 WebSocket 握手的 OAuth2 认证

WebSocket 握手是标准 HTTP 请求,可直接复用 Gateway 的 OAuth2 资源服务器能力,同时添加自定义过滤器注入用户ID到请求头,并将用户信息存入会话属性。

1.1 依赖配置

确保 Gateway 引入 OAuth2 资源服务器依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

1.2 路由与过滤器配置

在 Gateway 配置类中定义 WebSocket 路由,添加 OAuth2 认证过滤器和自定义会话绑定过滤器:

@Configuration
public class GatewayConfig {

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                // WebSocket 路由匹配
                .route("websocket_route", r -> r.path("/ws/**")
                        .filters(f -> f
                                // OAuth2 资源服务器认证,验证请求中的 Token
                                .oauth2ResourceServer()
                                // 自定义过滤器:提取认证后的用户ID,注入请求头并存入会话属性
                                .filter(new WebSocketAuthFilter()))
                        .uri("lb://your-websocket-service")) // 后端微服务地址
                // REST 接口路由(复用现有 OAuth2 配置即可)
                .route("rest_route", r -> r.path("/api/**")
                        .filters(f -> f.oauth2ResourceServer())
                        .uri("lb://your-rest-service"))
                .build();
    }
}

1.3 自定义 WebSocketAuthFilter

这个过滤器负责在握手阶段提取认证后的用户ID,注入请求头,并将用户信息存入 WebSocket 会话的属性中:

public class WebSocketAuthFilter implements GatewayFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 从 OAuth2 认证上下文获取用户信息
        Authentication authentication = exchange.getPrincipal().block();
        if (authentication == null) {
            return exchange.getResponse().setComplete(); // 未认证直接拒绝
        }
        String userId = authentication.getName(); // 假设用户ID是认证主体的名称,可根据实际调整

        // 1. 注入用户ID到HTTP请求头,供后端握手时接收
        exchange.getRequest().mutate().header("X-User-ID", userId);

        // 2. 将用户ID存入WebSocket会话属性(后续处理STOMP帧时用)
        exchange.getAttributes().put("X-User-ID", userId);

        return chain.filter(exchange);
    }
}

步骤2:拦截并修改 STOMP 帧,注入用户信息

Spring Cloud Gateway 通过 WebSocketRoutingFilter 处理 WebSocket 消息,我们可以自定义 WebSocketHandlerDecoratorFactory 来拦截所有 WebSocket 消息,解析 STOMP 帧并注入用户ID。

2.1 自定义 WebSocketHandlerDecoratorFactory

这个工厂类会包装默认的 WebSocketHandler,在消息处理时解析 STOMP 帧,添加用户ID头:

@Component
public class StompFrameDecoratorFactory implements WebSocketHandlerDecoratorFactory {

    @Override
    public WebSocketHandler decorate(WebSocketHandler handler) {
        return new WebSocketHandler() {
            @Override
            public Mono<Void> handle(WebSocketSession session) {
                // 从握手阶段的会话属性中取出用户ID
                String userId = session.getAttributes().getOrDefault("X-User-ID", "").toString();

                // 包装会话,拦截 outgoing 消息(网关发往后端的消息)
                WebSocketSession decoratedSession = new WebSocketSessionDecorator(session) {
                    @Override
                    public Mono<Void> send(Publisher<WebSocketMessage> messages) {
                        return Flux.from(messages)
                                .map(message -> modifyStompFrame(message, userId))
                                .flatMap(super::send);
                    }
                };

                return handler.handle(decoratedSession);
            }
        };
    }

    // 解析并修改 STOMP 帧,添加用户ID头
    private WebSocketMessage modifyStompFrame(WebSocketMessage message, String userId) {
        if (message.getType() != WebSocketMessage.Type.TEXT) {
            return message; // 只处理文本类型的 STOMP 帧
        }

        String payload = message.getPayloadAsText();
        // 简单解析 STOMP 帧(实际可使用 Spring 的 StompDecoder 做严谨解析)
        if (payload.startsWith("CONNECT")) {
            // 在 CONNECT 帧中添加 X-User-ID 头
            String modifiedPayload = payload.replace("\naccept-version:", 
                    "\nX-User-ID:" + userId + "\naccept-version:");
            return message.getSession().textMessage(modifiedPayload);
        }
        // 其他 STOMP 帧(如 SEND、SUBSCRIBE)也可以按需添加用户头
        return message;
    }
}

2.2 注意事项

  • 若需要更严谨的 STOMP 帧解析,建议使用 Spring 提供的 StompDecoder 和 StompEncoder,避免手动字符串处理出现兼容性问题。
  • 确保网关与后端微服务的 STOMP 版本兼容,头信息的添加符合 STOMP 协议规范。

步骤3:后端微服务接收已认证的请求

后端微服务无需再做 OAuth2 认证,直接从 STOMP 头或握手请求头中获取用户ID即可:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new ChannelInterceptor() {
            @Override
            public Message<?> preSend(Message<?> message, MessageChannel channel) {
                // 从 STOMP 头获取用户ID
                StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
                if (StompCommand.CONNECT.equals(accessor.getCommand())) {
                    String userId = accessor.getFirstNativeHeader("X-User-ID");
                    // 将用户ID绑定到认证上下文,供后续业务使用
                    accessor.setUser(() -> new UsernamePasswordAuthenticationToken(userId, null, Collections.emptyList()));
                }
                return message;
            }
        });
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.14 05:40:32