在现代 Web 应用中,实时通信功能变得越来越重要。本文将带您一步一步地使用 Spring Boot 和 WebSocket 构建一个带有用户认证和消息存储功能的实时聊天应用。

项目结构

我们将构建以下功能模块:

  1. WebSocket 聊天功能
  2. 用户认证与授权
  3. 消息存储
  4. 基础前端界面

依赖配置

首先,在 pom.xml 中添加必要的依赖项:

xml复制代码<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Spring Boot Starter WebSocket -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    <!-- Spring Boot Starter Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!-- Spring Boot Starter Data JPA -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <!-- H2 Database (for simplicity) -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!-- Jackson (for JSON processing) -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
</dependencies>

WebSocket 配置

创建一个 WebSocket 配置类来注册 WebSocket 端点:

java复制代码import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new ChatHandler(), "/chat")
                .setAllowedOrigins("*");
    }
}

WebSocket 处理器

编写一个处理 WebSocket 消息的处理器:

java复制代码import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class ChatHandler extends TextWebSocketHandler {
    private final Map<String, WebSocketSession> sessions = Collections.synchronizedMap(new HashMap<>());
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        String userId = session.getUri().getQuery().split("=")[1];
        sessions.put(userId, session);
        broadcastOnlineUsers();
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        ChatMessage chatMessage = objectMapper.readValue(message.getPayload(), ChatMessage.class);
        WebSocketSession targetSession = sessions.get(chatMessage.getTargetUserId());
        if (targetSession != null) {
            targetSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(chatMessage)));
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        String userId = session.getUri().getQuery().split("=")[1];
        sessions.remove(userId);
        broadcastOnlineUsers();
    }

    private void broadcastOnlineUsers() throws Exception {
        OnlineUsersMessage onlineUsersMessage = new OnlineUsersMessage(sessions.keySet());
        String message = objectMapper.writeValueAsString(onlineUsersMessage);
        for (WebSocketSession session : sessions.values()) {
            session.sendMessage(new TextMessage(message));
        }
    }
}

WebSocket 消息类

java复制代码public class ChatMessage {
    private String senderUserId;
    private String targetUserId;
    private String message;

    // Getters and setters
}

public class OnlineUsersMessage {
    private Set<String> onlineUsers;

    public OnlineUsersMessage(Set<String> onlineUsers) {
        this.onlineUsers = onlineUsers;
    }

    // Getters and setters
}

用户认证与授权

使用 Spring Security 进行用户认证和授权:

安全配置类

java复制代码import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
            .antMatchers("/chat").authenticated()
            .and()
            .httpBasic();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

消息存储

使用 JPA 将聊天消息存储到数据库中:

消息实体类

java复制代码import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Message {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String senderUserId;
    private String targetUserId;
    private String message;

    // Getters and setters
}

消息存储库接口

java复制代码import org.springframework.data.jpa.repository.JpaRepository;

public interface MessageRepository extends JpaRepository<Message, Long> {
}

消息服务类

java复制代码import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MessageService {

    @Autowired
    private MessageRepository messageRepository;

    public Message saveMessage(Message message) {
        return messageRepository.save(message);
    }
}

消息控制器

java复制代码import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ChatController {

    @Autowired
    private MessageService messageService;

    @PostMapping("/saveMessage")
    public Message saveMessage(@RequestBody Message message) {
        return messageService.saveMessage(message);
    }
}

前端实现

使用简单的 HTML 和 JavaScript 创建一个基本的聊天界面:

html复制代码<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Chat Application</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css">
    <style>
        /* 添加必要的CSS样式 */
    </style>
</head>
<body>
    <div id="userList" class="p-3"></div>
    <div id="chatPopup" class="p-3" style="display: none;">
        <div id="chatPopupHeader" class="d-flex justify-content-between align-items-center">
            <div id="chatPopupHeaderContent"></div>
            <button id="closeChatPopupButton">×</button>
        </div>
        <div id="chatPopupBody" class="overflow-auto p-2" style="height: 400px;"></div>
        <div id="chatPopupFooter" class="d-flex p-2">
            <input type="text" id="chatInput" class="form-control me-2" placeholder="Type a message...">
            <button id="sendMessageButton" class="btn btn-primary">Send</button>
        </div>
    </div>

    <script>
        const senderUserId = "user1"; // 假设这是当前用户ID
        let recipientId = ""; // 接收消息的用户ID
        const ws = new WebSocket('ws://localhost:8080/chat'); // 你的 WebSocket URL

        ws.onopen = function() {
            console.log('Connected to WebSocket');
        };

        ws.onmessage = function(event) {
            const message = JSON.parse(event.data);
            displayMessage(message.message, 'received');
        };

        document.getElementById("sendMessageButton").addEventListener("click", sendMessage);

        function sendMessage() {
            const messageInput = document.getElementById("chatInput").value;
            if (messageInput.trim() !== '') {
                const message = {
                    senderUserId: senderUserId,
                    message: messageInput,
                    targetUserId: recipientId.trim() !== '' ? recipientId : null
                };
                ws.send(JSON.stringify(message));

                displayMessage(messageInput, 'sent');
                document.getElementById("chatInput").value = '';
            }
        }

        function displayMessage(message, type) {
            const messageDiv = document.createElement("div");
            messageDiv.textContent = message;
            messageDiv.className = `col-12 message ${type}`;
            document.getElementById("chatPopupBody").appendChild(messageDiv);
        }

        // 获取在线用户并渲染
        function getOnlineUsers() {
            // 使用 AJAX 或 Fetch API 调用后端获取在线用户列表
        }

        // 渲染用户列表
        function renderUserList(users) {
            const userList = document.getElementById("userList");
            userList.innerHTML = '';
            users.forEach(user => {
                const userDiv = document.createElement("div");
                userDiv.className = `user ${user.online ? '' : 'offline'}`;
                userDiv.textContent = user.name;
                userDiv.addEventListener('click', () => {
                    recipientId = user.id;
                    document.getElementById("chatPopup").style.display = 'block';
                });
                userList.appendChild(userDiv);
            });
        }

        // 示例调用
        getOnlineUsers();
    </script>
</body>
</html>

总结

本文详细介绍了如何使用 Spring Boot 和 WebSocket 构建一个带有用户认证和消息存储功能的实时聊天应用。通过这些步骤,您可以轻松地创建一个功能齐全且安全的聊天应用,并根据自己的需求进行扩展和优化。