SpringBoot从0-1集成STOMP协议快速实现消息转发

12 阅读4分钟

🎨 前言

在即时的消息通信应用开发时,经常需要用到将消息转发给特定人员或群组的功能。这时如果使用WebSocket进行开发,就需要维护发送者和接收者的对应关系,当用户过多时,维护这个关系表会是很大的成本。这时我们就可以使用基于Websocket的STOMP协议,来进行消息的转发处理。

本篇文章就是记录SpringBoot如何集成STOMP协议转发的,但在此之前先来看看什么是STOMP协议。

❓ 什么是STOMP协议?

WebSocket 是浏览器与服务器之间的一种双向通信协议(属于 TCP 层之上的应用层协议),解决了 HTTP “请求 - 响应” 单向通信的痛点。

STOMP(Simple Text Oriented Messaging Protocol,简单文本定向消息协议)基于WebSocket协议的应用层的消息规范,可以理解为 “给 WebSocket 通信制定的‘普通话’”,在 WebSocket 底层通道之上,定义了统一的消息格式、交互规则、路由机制。

来看看如何集成吧!

Let‘s GO!

🌲 环境准备

Pom依赖

要使用STOMP协议需要用到spring-boot-starter-websocket 依赖,在Pom文件的dependencies中添加以下内容:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

🐎 代码步骤

示例中我们实现如下功能:

用户可以连接到ws://127.0.0.1:8080/ws/plot 并完成用户认证,并可订阅/topic/plot/{ID}路径,当用户发布数据到/send/plot/{ID} 路径时,将数据转发到所有的订阅了/topic/plot/{ID} 的用户。

1.配置类

首先配置类实现WebSocketMessageBrokerConfigurer 接口,并使用@EnableWebSocketMessageBroker 进行标记这是一个默认的Websocket消息代理,并实现以下方法:

registerStompEndpoints:在该方法中可以配置作为STOMP的路径,并可配置允许跨域访问。(即用户的WebSocket需要连接这个路径)

configureMessageBroker: 在该方法中可以设置作为订阅的代理路径。(即用户可以在这个路径上订阅)

configureClientInboundChannel:配置消息入站通道,如可以在这里配置拦截器对发起的请求进行认证。(这里是自定义的StompTokenValidateInterceptor 类作为拦截器)

具体代码如下:

package com.uav.qd.config;

import com.uav.qd.interceptor.StompTokenValidateInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

/**
 * @author nodcat
 * @version 1.0
 * @since 2025/12/29 下午4:44
 */
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketPlotConfig implements WebSocketMessageBrokerConfigurer {

    @Autowired
    StompTokenValidateInterceptor stompTokenValidateInterceptor;
    /**
     * 注册STOMP端点,供前端连接
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 标绘协同端点,允许跨域访问,支持WebSocket握手
        registry.addEndpoint("/ws/plot")
                .setAllowedOriginPatterns("*");
    }

    /**
     * 配置消息代理,实现消息广播
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 订阅地址
        registry.enableSimpleBroker("/topic/plot");
    }

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

2.用户认证

在用户认证时需要让StompTokenValidateInterceptor实现ChannelInterceptor 接口,并重写preSend 方法,这里的认证就可以自定义了,包括从请求头中获取token(或JWT TOKEN),然后查数据库(如果是JWT TOKEN则直接解析认证),通过后返回message即可完成连接。

代码如下:

package com.uav.qd.interceptor;

import com.uav.qd.common.constant.Constant;
import com.uav.qd.common.exception.SysException;
import com.uav.qd.common.security.SecurityUser;
import com.uav.qd.entity.SysUserTokenEntity;
import com.uav.qd.service.ProjectService;
import com.uav.qd.service.SysShiroService;
import com.uav.qd.service.SysUserService;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.stereotype.Component;

import java.util.Objects;

/**
 * @author nodcat
 * @version 1.0
 * @since 2026/1/4 上午11:43
 */
@Component
public class StompTokenValidateInterceptor implements ChannelInterceptor {

    @Autowired
    SysShiroService shiroService;

    private static final String AUTHORIZATION_HEADER = "token";

    @Override
    public Message<?> preSend(@NotNull Message<?> message, MessageChannel channel) {
        // 1. 获取 STOMP 头访问器,用于读取 STOMP 协议头
        StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
        if (accessor == null) {
            return message;
        }
        if (StompCommand.CONNECT.equals(accessor.getCommand())) {
            // 3. 提取 STOMP 连接头中的 Token
            String token = accessor.getFirstNativeHeader(AUTHORIZATION_HEADER);
            if (token == null || token.trim().isEmpty()) {
                throw new SysException("STOMP认证失败:未携带有效Token");
            }
            //这里是查询数据库获取,可根据实际需要修改逻辑
            SysUserTokenEntity userEntity = shiroService.getByToken(token);
            if (userEntity==null){
                throw new SysException("STOMP认证失败:Token不存在或已失效");
            }
            Long userId = userEntity.getUserId();
            Objects.requireNonNull(accessor.getSessionAttributes()).put(Constant.USER_KEY,userId);
        }
        return message;
    }
}

⚠️ 注意:这里可以将用户id等数据放到accessor中,以方便在后面的收到消息中直接获取这些关键数据。

3. 消息转发

消息转发时只需要使用到两个注解进行消息的转发:

@SendTo("/topic/plot/{projectId}"):将消息转发到订阅了该主题的用户。

@MessageMapping("/send/plot/{projectId}"):接收该路径发布的消息。

package com.uav.qd.controller.dash;

import com.uav.qd.common.constant.Constant;
import com.uav.qd.common.exception.SysException;
import com.uav.qd.common.security.SecurityUser;
import com.uav.qd.dto.emplan.EmPlanMessageDTO;
import com.uav.qd.dto.emplan.OperationMessageDTO;
import com.uav.qd.service.PlotProjectService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.stereotype.Controller;

import java.util.Map;
import java.util.Objects;

/**
 * STOMP方案消息处理器
 * @author nodcat
 * @version 1.0
 * @since 2025/12/30 上午10:11
 */
@Controller
public class DashPlotMessageController {

    @Autowired
    PlotProjectService plotProjectService;
    /**
     * 向其它用户转发当前项目编辑消息
     * @param projectId 项目ID
     * @param message 消息
     * @param accessor 认证消息
     * @return 共享操作消息
     */
    @SendTo("/topic/plot/{projectId}")
    @MessageMapping("/send/plot/{projectId}")
    public EmPlanMessageDTO handlePlotMessage(
            @DestinationVariable String projectId,
            OperationMessageDTO message,
            SimpMessageHeaderAccessor accessor
    ) throws Exception {
        Long userId = getUserId(accessor);
        //更新缓存内容
        plotProjectService.updateCacheData(projectId,message);
        return EmPlanMessageDTO.builder()
                .userId(userId)
                .projectId(projectId)
                .operate(message.getOperate())
                .data(message.getData())
                .build();
    }

    /**
     * 获取认证消息内的用户ID
     * @param accessor 认证消息
     * @return 用户ID
     */
    public Long getUserId(SimpMessageHeaderAccessor accessor){
        Map<String, Object> attributes = accessor.getSessionAttributes();
        return (Long) Objects.requireNonNull(attributes).get(Constant.USER_KEY);
    }

}

整个写法与@RestController中方法的写法类似,可以根据实际需要对消息进行缓存操作等,这里返回的类会以Json字符串的格式返回。

⭕ 总结

通过使用STOMP协议可以很方便的实现对特定订阅用户进行数据转发,无需在后端维护对应关系表,并且实现起来并不困难,只需要简单的配置就可以使用类似于HTTP接口的方式进行逻辑功能的实现。赶紧去试试吧!