首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Spring Boot 構建私有即時通訊系統:WebSocket + STOMP + Spring Security 完整實戰

     編輯:Java 相關知識

架構設計

層級技術選型角色
基礎框架Spring Boot 2.x自動配置、啟動器
安全框架Spring Security認證授權、Principal 注入
通訊協議WebSocket + STOMP全雙工、幀協議、訊息路由
消息代理SimpleBroker (內存) / RabbitMQ (生產)訂閱分發、廣播、點對點
前端庫SockJS + STOMP.js降級傳輸、瀏覽器相容
模板引擎Thymeleaf頁面渲染
數據庫MongoDB聊天記錄持久化

Maven 依賴

<dependencies>
    <!-- WebSocket + STOMP -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    <!-- Undertow 替代 Tomcat(WebSocket 效能更優) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-undertow</artifactId>
    </dependency>
    <!-- Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!-- MongoDB -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-mongodb</artifactId>
    </dependency>
    <!-- Thymeleaf -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <!-- 前端 WebJars -->
    <dependency>
        <groupId>org.webjars</groupId>
        <artifactId>sockjs-client</artifactId>
        <version>1.6.1</version>
    </dependency>
    <dependency>
        <groupId>org.webjars</groupId>
        <artifactId>stomp-websocket</artifactId>
        <version>2.3.4</version>
    </dependency>
    <dependency>
        <groupId>org.webjars</groupId>
        <artifactId>bootstrap</artifactId>
        <version>5.3.0</version>
    </dependency>
    <dependency>
        <groupId>org.webjars</groupId>
        <artifactId>jquery</artifactId>
        <version>3.7.0</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.83</version>
    </dependency>
</dependencies>

配置文件

server:
  port: 8080

spring:
  data:
    mongodb:
      uri: mongodb://user:pass@localhost:27017/chatdb
  cache:
    type: caffeine

啟動類啟用 WebSocket

@SpringBootApplication
@EnableWebSocketMessageBroker
public class ChatApplication {
    public static void main(String[] args) {
        SpringApplication.run(ChatApplication.class, args);
    }
}

WebSocket + STOMP 配置核心

@Configuration
@EnableWebSocketMessageBroker
@Slf4j
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 客戶端連接端點,支援 SockJS 降級
        registry.addEndpoint("/ws-chat")
                .setAllowedOriginPatterns("*")
                .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 客戶端發送消息的目標前綴
        registry.setApplicationDestinationPrefixes("/app");
        
        // 簡單代理:主題前綴
        registry.enableSimpleBroker("/topic", "/queue");
        
        // 用戶專屬隊列前綴(點對點)
        registry.setUserDestinationPrefix("/user");
    }

    @Override
    public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
        registration.setMessageSizeLimit(64 * 1024)  // 64KB
                    .setSendTimeLimit(15 * 1000)
                    .setSendBufferSizeLimit(512 * 1024);
        
        // 連接建立/關閉事件監聽
        registration.addDecoratorFactory(new WebSocketHandlerDecoratorFactory() {
            @Override
            public WebSocketHandler decorate(WebSocketHandler handler) {
                return new WebSocketHandlerDecorator(handler) {
                    @Override
                    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
                        String username = session.getPrincipal().getName();
                        log.info("用戶上線: {}", username);
                        // 可在此處發送在線用戶列表、未讀消息等
                        super.afterConnectionEstablished(session);
                    }

                    @Override
                    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
                        String username = session.getPrincipal().getName();
                        log.info("用戶下線: {}", username);
                        super.afterConnectionClosed(session, status);
                    }
                };
            }
        });
    }
}

控制器:處理群聊與私聊

@Controller
@Slf4j
@RequiredArgsConstructor
public class ChatController {
    private final SimpMessagingTemplate messagingTemplate;
    private final ChatMessageService messageService;

    /** 群聊廣播:/app/chat.broadcast -> /topic/public */
    @MessageMapping("/chat.broadcast")
    public void broadcast(Principal principal, @Payload ChatMessageRequest request) {
        String from = principal.getName();
        ChatMessage msg = ChatMessage.builder()
            .fromUser(from)
            .content(request.getContent())
            .type(MessageType.PUBLIC)
            .timestamp(Instant.now())
            .build();
        
        messageService.save(msg);
        
        // 廣播給所有訂閱 /topic/public 的在線用戶
        messagingTemplate.convertAndSend("/topic/public", msg);
    }

    /** 私聊:/app/chat.private -> /user/{toUser}/queue/private */
    @MessageMapping("/chat.private")
    public void privateChat(Principal principal, @Payload PrivateChatRequest request) {
        String from = principal.getName();
        String to = request.getToUser();
        
        ChatMessage msg = ChatMessage.builder()
            .fromUser(from)
            .toUser(to)
            .content(request.getContent())
            .type(MessageType.PRIVATE)
            .timestamp(Instant.now())
            .build();
        
        messageService.save(msg);
        
        // 發送給特定用戶(無論是否在線,離線消息由 SimpleBroker 暫存)
        messagingTemplate.convertAndSendToUser(to, "/queue/private", msg);
        
        // 同時回送給發送者確認
        messagingTemplate.convertAndSendToUser(from, "/queue/private", msg);
    }

    /** 獲取歷史消息 */
    @MessageMapping("/chat.history")
    public void history(Principal principal, @Payload HistoryRequest request) {
        String user = principal.getName();
        List<ChatMessage> history = messageService.getHistory(
            user, request.getTargetUser(), request.getType(), request.getLimit()
        );
        messagingTemplate.convertAndSendToUser(user, "/queue/history", history);
    }
}

前端頁面與 JavaScript 邏輯

Thymeleaf 頁面片段

<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
    <title>即時通訊演示</title>
    <link th:href="@{/webjars/bootstrap/5.3.0/css/bootstrap.min.css}" rel="stylesheet" />
    <script th:src="@{/webjars/sockjs-client/1.6.1/sockjs.min.js}"></script>
    <script th:src="@{/webjars/stomp-websocket/2.3.4/stomp.min.js}"></script>
    <script th:src="@{/webjars/jquery/3.7.0/jquery.min.js}"></script>
</head>
<body>
    <div class="container mt-4">
        <div class="row">
            <div class="col-md-4">
                <h5>在線用戶</h5>
                <ul id="userList" class="list-group"></ul>
            </div>
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header">
                        <select id="chatMode" class="form-select d-inline w-auto">
                            <option value="public">大廳群聊</option>
                            <option value="private">私聊</option>
                        &select>
                        <input id="targetUser" type="text" class="form-control d-inline w-auto" placeholder="私聊目標用戶" style="display:none">
                    </div>
                    <div class="card-body" style="height: 400px; overflow-y: auto;">
                        <div id="messages"></div>
                    </div>
                    <div class="card-footer">
                        <div class="input-group">
                            <input id="messageInput" type="text" class="form-control" placeholder="輸入消息...">
                            <button id="sendBtn" class="btn btn-primary">發送</button>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <script th:inline="javascript">
        /**/
    </script>
    <script th:src="@{/js/chat.js}"></script>
</body>
</html>

chat.js 核心邏輯

let stompClient = null;
const username = document.currentScript.getAttribute('data-username') || 'anonymous';

function connect() {
    const socket = new SockJS('/ws-chat');
    stompClient = Stomp.over(socket);
    stompClient.debug = null; // 關閉調試日誌
    
    stompClient.connect({}, function(frame) {
        console.log('已連接: ' + frame);
        
        // 1. 訂閱群聊廣播
        stompClient.subscribe('/topic/public', function(message) {
            showMessage(JSON.parse(message.body), 'public');
        });
        
        // 2. 訂閱私聊隊列
        stompClient.subscribe('/user/queue/private', function(message) {
            showMessage(JSON.parse(message.body), 'private');
        });
        
        // 3. 訂閱歷史消息回調
        stompClient.subscribe('/user/queue/history', function(message) {
            renderHistory(JSON.parse(message.body));
        });
        
        // 4. 請求在線用戶列表(可選)
        // stompClient.send("/app/user.online", {}, JSON.stringify({}));
    }, function(error) {
        console.error('連接失敗:', error);
        setTimeout(connect, 5000); // 重連
    });
}

function sendMessage() {
    const input = $('#messageInput');
    const content = input.val().trim();
    if (!content) return;
    
    const mode = $('#chatMode').val();
    if (mode === 'public') {
        stompClient.send("/app/chat.broadcast", {}, JSON.stringify({ content }));
    } else {
        const target = $('#targetUser').val().trim();
        if (!target) { alert('請輸入目標用戶'); return; }
        stompClient.send("/app/chat.private", {}, JSON.stringify({ 
            content, toUser: target 
        }));
    }
    input.val('');
}

function showMessage(msg, type) {
    const div = document.createElement('div');
    div.className = 'message ' + (msg.fromUser === username ? 'sent' : 'received');
    const time = new Date(msg.timestamp).toLocaleTimeString();
    div.innerHTML = `${msg.fromUser} ${time}
${msg.content}`; $('#messages').append(div).scrollTop($('#messages')[0].scrollHeight); } $('#sendBtn').click(sendMessage); $('#messageInput').keypress(e => e.which === 13 && sendMessage()); $('#chatMode').change(function() { $('#targetUser').toggle($(this).val() === 'private'); }); $(document).ready(connect);

Spring Security 整合要點

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/", "/login", "/webjars/**", "/ws-chat/**").permitAll()
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginPage("/login").permitAll()
            .and()
            .logout().permitAll()
            .and()
            .csrf().ignoringAntMatchers("/ws-chat/**"); // WebSocket 握手豁免 CSRF
    }
}

關鍵:Principal 自動從 SecurityContext 獲取,@MessageMapping 方法參數直接注入當前用戶。

生產環境擴展:引入 RabbitMQ 作為外部 Broker

@Configuration
public class RabbitMqBrokerConfig {
    @Bean
    public MessageBrokerRegistry configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableStompBrokerRelay("/topic", "/queue")
                .setRelayHost("localhost")
                .setRelayPort(61613)
                .setClientLogin("guest")
                .setClientPasscode("guest")
                .setSystemLogin("system")
                .setSystemPasscode("system");
        return registry;
    }
}

優勢:多實例水平擴展、消息持久化、集群高可用。

效果演示

啟動三個瀏覽器窗口以不同用戶登錄:

  • 群聊:用戶 A 發送,B/C 實時收到
  • 私聊:A 發送給 B,僅 B 收到,C 不收到
  • 離線消息:用戶離線時發送,上線後自動接收(SimpleBroker 暫存)
  • 歷史記錄:點擊載入歷史,從 MongoDB 讀取渲染

總結

Spring Boot + WebSocket + STOMP 構建即時通訊系統核心要點:

  1. @EnableWebSocketMessageBroker 啟用 STOMP over WebSocket
  2. registerStompEndpoints 註冊 /ws-chat + SockJS 降級
  3. configureMessageBroker 定義 /app(入站)、/topic(廣播)、/user(點對點)
  4. SimpMessagingTemplate 服務端主動推送:convertAndSend(廣播)、convertAndSendToUser(私聊)
  5. Spring Security 無縫整合,Principal 自動注入
  6. 前端:SockJS + STOMP.js,訂閱 /topic/public、/user/queue/private

由內存 SimpleBroker 起步,平滑遷移至 RabbitMQ/ActiveMQ 實現生產級擴展。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved