加拿大机器人 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示例代码,包含连接建立、消息收发、离线消息渲染的完整页面逻辑吗?