如何在Spring API Gateway中拦截并处理STOMP请求?
Spring API Gateway 处理 STOMP 请求认证的实现方案
核心思路
STOMP 基于 WebSocket,分为HTTP 握手阶段和STOMP 帧传输阶段,网关需要分别处理这两个阶段的认证与用户信息传递:
- 握手阶段:利用 Gateway 的 OAuth2 能力完成认证,将用户ID注入握手请求头,同时把用户信息绑定到 WebSocket 会话。
- 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
相关产品推荐
相关产品推荐

