置顶

加拿大机器人 SpringBoot 即时聊天 IM 完整实现(HTTP会话管理 + WebSocket实时推送 + 离线消息)

作者:admin | 分类:加拿大机器人 | 浏览:2 | 日期:2026年07月10日



很多新手做即时聊天功能时,要么直接裸写WebSocket忽略了身份安全校验,要么只做了在线消息推送漏掉离线场景,最后上线后出现消息乱发、用户身份伪造、离线后消息丢失等一堆问题。下面这套完整实现方案,从会话层到消息层全链路闭环,直接复制代码就能在SpringBoot里跑通一个生产可用的轻量IM系统。


一、整体架构分层设计


整个系统拆成三层,完全解耦避免后续改需求动全量代码:


HTTP会话管理层‌:复用SpringSecurity的登录态体系,统一做身份校验,拒绝未登录用户随意连接WebSocket,从根源上避免身份伪造问题。

WebSocket实时推送层‌:维护在线用户的长连接会话池,做消息的实时转发,支持单聊、群聊两种场景的消息即时投递。

离线消息持久化层‌:用户不在线时自动把消息落库,等用户下次上线时自动拉取未读消息,保证消息100%不丢失。

这套架构完全不用引入额外的IM中间件,几十行核心代码就能跑通,适合中小项目快速接入即时聊天能力,后续流量上来了再无缝迁移到Redis集群做分布式扩展。

二、第一步:HTTP会话管理,把身份校验做在连接之前


很多新手直接把WebSocket接口暴露出去,完全不做身份校验,导致任何人都能连上来发消息,这是最常见的安全漏洞。我们直接复用SpringBoot的HTTP登录态体系,在握手阶段就把身份校验做完:


先在pom.xml引入核心依赖,包含Spring Web、WebSocket、SpringSecurity、MyBatis-Plus和MySQL驱动:

xml

<dependencies>

    <dependency>

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter-web</artifactId>

    </dependency>

    <dependency>

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter-websocket</artifactId>

    </dependency>

    <dependency>

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter-security</artifactId>

    </dependency>

    <dependency>

        <groupId>com.baomidou</groupId>

        <artifactId>mybatis-plus-boot-starter</artifactId>

        <version>3.5.7</version>

    </dependency>

    <dependency>

        <groupId>com.mysql</groupId>

        <artifactId>mysql-connector-j</artifactId>

        <scope>runtime</scope>

    </dependency>

</dependencies>


实现自定义握手拦截器,从HTTP会话里取出当前登录用户的ID,校验通过之后才允许建立WebSocket连接,直接把非法请求拦在连接建立之前:

java

@Component

public class HttpSessionHandshakeInterceptor implements HandshakeInterceptor {

    @Override

    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {

        // 从当前HTTP会话中取出已登录的用户ID

        ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;

        HttpSession session = servletRequest.getServletRequest().getSession();

        Long currentUserId = (Long) session.getAttribute("currentUserId");

        // 未登录用户直接拒绝握手

        if (currentUserId == null) {

            response.setStatusCode(HttpStatus.UNAUTHORIZED);

            return false;

        }

        // 把用户ID存入WebSocket连接属性中,后续直接取用

        attributes.put("userId", currentUserId);

        return true;

    }


    @Override

    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {

    }

}


配置WebSocket注册,把拦截器绑定到对应的端点路径上:

java

@Configuration

@EnableWebSocket

public class WebSocketConfig implements WebSocketConfigurer {

    @Resource

    private HttpSessionHandshakeInterceptor handshakeInterceptor;

    @Resource

    private ChatMessageHandler chatMessageHandler;


    @Override

    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

        registry.addHandler(chatMessageHandler, "/im/ws")

                .addInterceptors(handshakeInterceptor)

                // 开发环境允许跨域,生产环境替换成你的前端域名

                .setAllowedOrigins("*");

    }

}


三、第二步:WebSocket实时推送,维护在线会话池


实现自定义消息处理器,维护一个全局的在线用户会话Map,用户连接上来就把会话存进去,断开连接就自动移除,收到消息之后直接推送给目标用户:


java

@Component

public class ChatMessageHandler extends TextWebSocketHandler {

    // 全局在线用户会话池,Key是用户ID,Value是对应的WebSocket会话

    private static final ConcurrentHashMap<Long, WebSocketSession> ONLINE_SESSION_MAP = new ConcurrentHashMap<>();

    @Resource

    private OfflineMessageService offlineMessageService;


    // 用户连接建立成功时触发

    @Override

    public void afterConnectionEstablished(WebSocketSession session) throws Exception {

        Long userId = (Long) session.getAttributes().get("userId");

        ONLINE_SESSION_MAP.put(userId, session);

        // 用户刚上线,自动拉取所有未读离线消息

        offlineMessageService.pullOfflineMessages(userId);

    }


    // 收到前端发来的聊天消息时触发

    @Override

    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {

        // 解析消息体,包含接收人ID、消息内容、消息类型

        ChatMessage chatMessage = new ObjectMapper().readValue(message.getPayload(), ChatMessage.class);

        Long receiverId = chatMessage.getReceiverId();

        

        // 目标用户在线,直接实时推送消息

        if (ONLINE_SESSION_MAP.containsKey(receiverId) && ONLINE_SESSION_MAP.get(receiverId).isOpen()) {

            WebSocketSession receiverSession = ONLINE_SESSION_MAP.get(receiverId);

            receiverSession.sendMessage(new TextMessage(new ObjectMapper().writeValueAsString(chatMessage)));

            return;

        }

        

        // 目标用户不在线,把消息存入离线消息库

        offlineMessageService.saveOfflineMessage(chatMessage);

    }


    // 连接断开时触发,自动清理在线会话

    @Override

    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {

        Long userId = (Long) session.getAttributes().get("userId");

        ONLINE_SESSION_MAP.remove(userId);

    }

}


四、第三步:离线消息闭环,保证消息100%不丢失


设计离线消息表结构,用户不在线时自动落库,下次用户上线时自动批量拉取所有未读消息,标记为已读,彻底避免消息丢失:


离线消息表SQL设计:

sql

CREATE TABLE `im_offline_message` (

  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',

  `sender_id` bigint NOT NULL COMMENT '发送人ID',

  `receiver_id` bigint NOT NULL COMMENT '接收人ID',

  `content` text NOT NULL COMMENT '消息内容',

  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '消息发送时间',

  `is_read` tinyint NOT NULL DEFAULT '0' COMMENT '是否已读 0未读 1已读',

  PRIMARY KEY (`id`),

  KEY `idx_receiver_id` (`receiver_id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='IM离线消息表';


离线消息服务核心实现,用户上线时自动拉取所有未读消息:

java

@Service

public class OfflineMessageService {

    @Resource

    private OfflineMessageMapper offlineMessageMapper;


    // 保存离线消息

    public void saveOfflineMessage(ChatMessage message) {

        ImOfflineMessage offlineMessage = new ImOfflineMessage();

        BeanUtils.copyProperties(message, offlineMessage);

        offlineMessageMapper.insert(offlineMessage);

    }


    // 用户上线拉取所有未读离线消息

    public void pullOfflineMessages(Long userId) throws JsonProcessingException {

        List<ImOfflineMessage> unreadList = offlineMessageMapper.selectList(

                new LambdaQueryWrapper<ImOfflineMessage>()

                        .eq(ImOfflineMessage::getReceiverId, userId)

                        .eq(ImOfflineMessage::getIsRead, 0)

        );

        

        WebSocketSession session = ONLINE_SESSION_MAP.get(userId);

        if (session != null && session.isOpen()) {

            for (ImOfflineMessage msg : unreadList) {

                ChatMessage chatMsg = new ChatMessage();

                BeanUtils.copyProperties(msg, chatMsg);

                session.sendMessage(new TextMessage(new ObjectMapper().writeValueAsString(chatMsg)));

            }

        }

        

        // 拉取完成后批量标记为已读

        if (!unreadList.isEmpty()) {

            offlineMessageMapper.update(null,

                    new LambdaUpdateWrapper<ImOfflineMessage>()

                            .eq(ImOfflineMessage::getReceiverId, userId)

                            .eq(ImOfflineMessage::getIsRead, 0)

                            .set(ImOfflineMessage::getIsRead, 1)

            );

        }

    }

}


五、生产环境避坑指南

给WebSocket连接加心跳机制,前端每隔30秒发一个ping消息,服务端没有收到心跳就自动断开连接,避免僵死会话占满资源。

分布式部署时,把在线会话池迁移到Redis,用Redis的Pub/Sub做跨节点消息推送,不同服务器上的用户也能互相发消息。

消息体加唯一ID,前端做消息去重,避免网络重传导致同一条消息重复收到的问题。

这套完整实现跑通之后,你就能得到一个支持身份校验、实时推送、离线消息全闭环的轻量IM系统,完全满足中小项目的即时聊天需求。


需要我给你配套一份前端对接的Vue3示例代码,包含连接建立、消息收发、离线消息渲染的完整页面逻辑吗?