在 React 应用程序中构建安全的实时聊天:使用 Socket.IO 和 CryptoJS 的指南

虽然端到端加密是信息安全的黄金标准,但许多平台需要更灵活的方法。企业通常需要信息访问来解决争议、保证质量或遵守法律。本文展示了一种兼顾安全性和业务需求的实用解决方案。

对于刚刚开始实施安全聊天的人来说,与端到端加密相比,使用 CryptoJS 的方法提供了一个简单得多的切入点。基本实现简单明了,易于理解和维护,同时还能为大多数业务案例提供强大的安全性。它避免了端到端加密带来的密钥管理和多设备同步的复杂性。

本文将介绍

  • 使用 Socket.IO 设置服务器
  • 消息加密
  • 实时通信
  • 基本 UI 实现
  • 错误处理

服务器实现

// server/index.ts
import express from 'express';
import { createServer } from 'http';
import { Server } from 'socket.io';
import cors from 'cors';
import { encryptMessage, decryptMessage } from './encryption';

const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
  cors: { origin: "http://localhost:3000" } //url
});

// 存储已连接的用户
const connectedUsers = new Map<string, string>();

app.use(cors());
app.use(express.json());

io.on('connection', (socket) => {
  const userId = socket.handshake.query.userId as string;
  connectedUsers.set(userId, socket.id);

  socket.on('message:send', async (data) => {
    try {
      // 将加密信息保存到数据库,适应您的数据库
      const message = await Message.create({
        senderId: userId,
        receiverId: data.receiver,
        content: data.content, // Already encrypted from client
        timestamp: new Date()
      });

      // 如果在线,转发给receiver
      const receiverSocketId = connectedUsers.get(data.receiver);
      if (receiverSocketId) {
        io.to(receiverSocketId).emit('message:received', {
          id: message._id,
          content: data.content,
          sender: userId,
          timestamp: new Date()
        });
      }
    } catch (error) {
      socket.emit('message:error', 'Failed to send message');
    }
  });

  socket.on('disconnect', () => {
    connectedUsers.delete(userId);
  });
});

httpServer.listen(3001, () => {
  console.log('Server running on port 3001');
});

前端实现

// hooks/useChat.ts
import { useEffect, useRef, useState, useCallback } from 'react';
import { io, Socket } from 'socket.io-client';
import { encryptMessage, decryptMessage } from '../lib/encryption';

interface Message {
  id: string;
  content: string;
  sender: string;
  timestamp: Date;
}

export const useChat = (userId: string, receiverId: string) => {
  const [messages, setMessages] = useState<Message[]>([]);
  const [isConnected, setIsConnected] = useState(false);
  const socket = useRef<Socket | null>(null);

  useEffect(() => {
    socket.current = io('http://localhost:3001', {
      query: { userId }
    });

    socket.current.on('connect', () => {
      setIsConnected(true);
    });

    socket.current.on('message:received', (message) => {
      const decryptedContent = decryptMessage(message.content);
      setMessages(prev => [...prev, {
        ...message,
        content: decryptedContent
      }]);
    });

    socket.current.on('message:error', (error) => {
      console.error('Message error:', error);
    });

    return () => {
      socket.current?.disconnect();
    };
  }, [userId]);

  const sendMessage = useCallback((content: string) => {
    if (!socket.current || !content.trim()) return;

    const encryptedContent = encryptMessage(content);
    
    socket.current.emit('message:send', {
      receiver: receiverId,
      content: encryptedContent
    });

    // 为 state 添加信息
    setMessages(prev => [...prev, {
      id: Date.now().toString(),
      content,
      sender: userId,
      timestamp: new Date()
    }]);
  }, [userId, receiverId]);

  return { messages, sendMessage, isConnected };
};

加密

// lib/encryption.ts
import CryptoJS from 'crypto-js';

//生成自己的密钥
const ENCRYPTION_KEY = process.env.NEXT_PUBLIC_ENCRYPTION_KEY!; 

export const encryptMessage = (message: string): string => {
  return CryptoJS.AES.encrypt(message, ENCRYPTION_KEY).toString();
};

export const decryptMessage = (encryptedMessage: string): string => {
  const bytes = CryptoJS.AES.decrypt(encryptedMessage, ENCRYPTION_KEY);
  return bytes.toString(CryptoJS.enc.Utf8);
};

聊天组件

import { useChat } from '../hooks/useChat';

export const Chat = ({ userId, receiverId }: { 
  userId: string; 
  receiverId: string;
}) => {
  const { messages, sendMessage, isConnected } = useChat(userId, receiverId);
  const [input, setInput] = useState('');

  const handleSend = () => {
    if (!input.trim()) return;
    sendMessage(input);
    setInput('');
  };

  return (
    <div className="flex flex-col h-screen">
      {!isConnected && (
        <div className="bg-yellow-100 p-2">
          Connecting...
        </div>
      )}
      
      <div className="flex-1 overflow-y-auto p-4">
        {messages.map((msg) => (
          <div 
            key={msg.id} 
            className={`mb-2 ${
              msg.sender === userId ? 'text-right' : 'text-left'
            }`}
          >
            <div className={`inline-block p-2 rounded-lg ${
              msg.sender === userId 
                ? 'bg-blue-500 text-white' 
                : 'bg-gray-200'
            }`}>
              {msg.content}
            </div>
          </div>
        ))}
      </div>

      <div className="border-t p-4 flex gap-2">
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && handleSend()}
          className="flex-1 p-2 border rounded"
          placeholder="Type a message..."
        />
        <button 
          onClick={handleSend}
          className="px-4 py-2 bg-blue-500 text-white rounded"
        >
          Send
        </button>
      </div>
    </div>
  );
};

消息流程

  • 用户输入信息并点击发送
  • 消息在客户端加密
  • 加密消息发送到服务器
  • 服务器将加密消息保存到数据库中
  • 服务器将加密消息转发给收件人
  • 收件人在客户端解密消息
  • 消息显示在聊天 UI 中

建立一个安全的聊天系统并不复杂。通过使用 Socket.IO 进行实时通信和 CryptoJS 进行基本加密,您可以创建一个强大的解决方案,在安全性和实际业务需求之间取得平衡。

作者:Svetlin Markov

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/im/54062.html

(0)

相关推荐

发表回复

登录后才能评论