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

Spring Boot WebSocket握手拦截器无法获取React端username header问题

解决SockJS+STOMP握手时无法获取username的问题

问题根源

你通过client.connect()传递的header是STOMP协议帧的专属header,并非HTTP握手阶段的HTTP请求头,所以在HandshakeInterceptor里无法读取到。另外跨域场景下,自定义HTTP头会被浏览器预检请求拦截,也会导致参数无法传递。

可行解决方案

方案1:通过URL Query参数传递username

这种方式能在HTTP握手阶段直接拿到参数,适合需要在握手环节就获取用户信息的场景。

前端修改

把username拼接到SockJS的连接URL后:

const socketFactory = () => {
    // 对username做URL编码,避免特殊字符问题
    return new SockJS(`http://localhost:8080/ws?username=${encodeURIComponent(username)}`);
};

const client = Stomp.over(socketFactory);

// 无需再在connect参数里传递username
client.connect({}, () => {
    if (mounted) {
        console.log('WebSocket Connected');
        setIsConnected(true);
        setError('');
        stompClientRef.current = client;

        subscriptionRef.current = client.subscribe('/topic/main', (response) => {
            try {
                const message = JSON.parse(response.body);
                setMessages((prev) => [...prev, message]);
            } catch (err) {
                console.error('Error parsing message:', err);
            }
        });
    }
});

后端修改

在HandshakeInterceptor中从Query参数提取username:

public class MyHandshakeInterceptor implements HandshakeInterceptor {

    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        // 从Query参数获取username并解码
        String username = request.getQueryParams().getFirst("username");
        if (username != null) {
            username = URLDecoder.decode(username, StandardCharsets.UTF_8.name());
            attributes.put("username", username);
            System.out.println("username from interceptor: " + username);
        } else {
            System.out.println("Username is null");
        }

        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
        // 空实现即可
    }
}

方案2:从STOMP Connect帧中获取username

如果不需要在HTTP握手阶段拿到username,而是在STOMP连接建立后获取,这种方式更符合STOMP协议的设计逻辑。

前端保持原有代码

继续在client.connect()的参数中传递username即可:

client.connect(
    {'username': username},
    () => {
        if (mounted) {
            console.log('WebSocket Connected');
            setIsConnected(true);
            setError('');
            stompClientRef.current = client;

            subscriptionRef.current = client.subscribe('/topic/main', (response) => {
                try {
                    const message = JSON.parse(response.body);
                    setMessages((prev) => [...prev, message]);
                } catch (err) {
                    console.error('Error parsing message:', err);
                }
            });
        }
    }
);

后端添加STOMP帧拦截器

创建ChannelInterceptor拦截STOMP的CONNECT帧,提取username并存入会话属性:

@Component
public class StompConnectInterceptor implements ChannelInterceptor {

    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
        
        if (StompCommand.CONNECT.equals(accessor.getCommand())) {
            String username = accessor.getFirstNativeHeader("username");
            if (username != null) {
                // 将username存入会话属性,后续事件监听可直接获取
                accessor.getSessionAttributes().put("username", username);
            }
        }
        
        return message;
    }
}

配置WebSocket注册拦截器

在WebSocket配置类中注册该拦截器:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Autowired
    private StompConnectInterceptor stompConnectInterceptor;

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(stompConnectInterceptor);
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
                .setAllowedOrigins("http://localhost:3000")
                .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic");
        registry.setApplicationDestinationPrefixes("/app");
    }
}

实现用户加入/离开通知

无论采用哪种方案,都可以通过监听WebSocket会话事件来广播系统消息:

@Component
public class WebSocketSessionListener {

    @Autowired
    private SimpMessagingTemplate messagingTemplate;

    // 监听用户连接事件
    @EventListener
    public void onSessionConnect(SessionConnectEvent event) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
        String username = accessor.getSessionAttributes().get("username").toString();
        messagingTemplate.convertAndSend("/topic/main", 
            new ChatMessage(username + " 加入了聊天", "SYSTEM"));
    }

    // 监听用户断开连接事件
    @EventListener
    public void onSessionDisconnect(SessionDisconnectEvent event) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
        String username = accessor.getSessionAttributes().get("username").toString();
        messagingTemplate.convertAndSend("/topic/main", 
            new ChatMessage(username + " 离开了聊天", "SYSTEM"));
    }
}

// 聊天消息实体类示例
public class ChatMessage {
    private String content;
    private String type;

    public ChatMessage(String content, String type) {
        this.content = content;
        this.type = type;
    }

    // getter和setter方法
    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }
}

内容的提问来源于stack exchange,提问作者Nishan Kumar Shrestha

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 08:38:13