You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Express搭建带注册、登录及聊天功能的服务器遇阻,求技术建议

Express 注册/登录+实时聊天服务器实现方案

一、核心技术选型

  • 后端框架:Express + jsonwebtoken(做身份验证,无状态更适合分布式场景)
  • 实时通信:Socket.io(基于WebSocket,原生支持跨域、重连,适配聊天场景)
  • 数据库:MongoDB + Mongoose(文档型数据库,灵活存储用户、聊天记录)
  • 密码加密:bcryptjs(不可逆加盐加密,避免明文存储风险)

二、注册/登录模块实现步骤

1. 用户模型定义(Mongoose)

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
  username: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  isOnline: { type: Boolean, default: false },
  socketId: { type: String } // 存储用户Socket连接ID,用于私聊
});

// 保存前自动加密密码
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  this.password = await bcrypt.hash(this.password, 10);
  next();
});

// 密码验证方法
userSchema.methods.checkPassword = async function(inputPwd) {
  return await bcrypt.compare(inputPwd, this.password);
};

module.exports = mongoose.model('User', userSchema);

2. 注册接口

const router = require('express').Router();
const User = require('../models/User');

router.post('/register', async (req, res) => {
  try {
    const { username, password } = req.body;
    // 检查用户名是否重复
    const existingUser = await User.findOne({ username });
    if (existingUser) return res.status(400).json({ msg: '用户名已被占用' });

    const newUser = new User({ username, password });
    await newUser.save();
    res.status(201).json({ msg: '注册成功' });
  } catch (err) {
    res.status(500).json({ msg: '服务器内部错误' });
  }
});

module.exports = router;

3. 登录接口(JWT版本)

const jwt = require('jsonwebtoken');
const User = require('../models/User');

router.post('/login', async (req, res) => {
  try {
    const { username, password } = req.body;
    const user = await User.findOne({ username });
    if (!user) return res.status(400).json({ msg: '用户不存在' });

    const isPwdValid = await user.checkPassword(password);
    if (!isPwdValid) return res.status(400).json({ msg: '密码错误' });

    // 生成有效期24小时的JWT令牌
    const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: '24h' });
    res.json({
      token,
      userInfo: { id: user._id, username: user.username }
    });
  } catch (err) {
    res.status(500).json({ msg: '服务器内部错误' });
  }
});

4. 身份验证中间件(保护接口/Socket连接)

const jwt = require('jsonwebtoken');
const User = require('../models/User');

const auth = async (req, res, next) => {
  const token = req.header('x-auth-token');
  if (!token) return res.status(401).json({ msg: '无权限,请先登录' });

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = await User.findById(decoded.userId).select('-password');
    next();
  } catch (err) {
    res.status(401).json({ msg: '令牌无效或已过期' });
  }
};

module.exports = auth;

三、实时聊天模块(Socket.io)

1. 服务器端配置

const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const jwt = require('jsonwebtoken');
const User = require('./models/User');

const app = express();
const server = http.createServer(app);
// 生产环境替换origin为前端域名
const io = socketIo(server, { cors: { origin: '*' } });

// Socket连接前鉴权
io.use(async (socket, next) => {
  const token = socket.handshake.auth.token;
  if (!token) return next(new Error('未携带登录令牌'));

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    const user = await User.findById(decoded.userId);
    if (!user) return next(new Error('用户不存在'));

    // 更新用户在线状态和SocketID
    await User.findByIdAndUpdate(user._id, { isOnline: true, socketId: socket.id });
    socket.user = user;
    next();
  } catch (err) {
    next(new Error('令牌无效'));
  }
});

// 处理Socket事件
io.on('connection', (socket) => {
  console.log(`${socket.user.username} 已上线`);
  // 广播所有用户在线列表
  io.emit('onlineUsers', await User.find({ isOnline: true }, 'username'));

  // 接收客户端发送的消息
  socket.on('sendMsg', async (data) => {
    // 全局广播消息
    io.emit('recvMsg', {
      from: socket.user.username,
      content: data.content,
      time: new Date().toLocaleString()
    });
    // 可选:保存聊天记录到数据库
    // await Message.create({ from: socket.user._id, content: data.content });
  });

  // 私聊功能
  socket.on('sendPrivateMsg', async (data) => {
    const targetUser = await User.findOne({ username: data.target });
    if (targetUser?.socketId) {
      io.to(targetUser.socketId).emit('recvPrivateMsg', {
        from: socket.user.username,
        content: data.content,
        time: new Date().toLocaleString()
      });
    }
  });

  // 用户断开连接
  socket.on('disconnect', async () => {
    await User.findByIdAndUpdate(socket.user._id, { isOnline: false, socketId: '' });
    console.log(`${socket.user.username} 已下线`);
    io.emit('onlineUsers', await User.find({ isOnline: true }, 'username'));
  });
});

server.listen(3000, () => console.log('服务器运行在 http://localhost:3000'));

2. 客户端基础示例(浏览器端)

// 登录后获取JWT令牌
const token = localStorage.getItem('token');
const socket = io('http://localhost:3000', { auth: { token } });

// 接收全局消息
socket.on('recvMsg', (msg) => {
  console.log(`${msg.from} [${msg.time}]: ${msg.content}`);
  // 渲染到页面聊天框
});

// 发送消息
document.getElementById('sendBtn').addEventListener('click', () => {
  const content = document.getElementById('msgInput').value.trim();
  if (content) {
    socket.emit('sendMsg', { content });
    document.getElementById('msgInput').value = '';
  }
});

四、常见问题排查

  • Socket连接失败:检查JWT令牌是否正确传递、CORS配置是否允许前端域名、令牌是否过期。
  • 消息无法广播:确认服务器端调用io.emit而非socket.emit,客户端是否监听了对应事件。
  • 密码验证失败:检查注册时是否触发了Mongoose预保存钩子,bcrypt哈希和比对是否使用相同盐值。
  • 在线状态更新异常:确保断开连接时的数据库更新语句正确,处理异步操作的错误捕获。

五、优化建议

  • 用Redis存储JWT黑名单,处理令牌主动注销需求。
  • 聊天记录添加分页查询,避免一次性加载大量数据。
  • 前端增加Socket重连机制,处理网络波动后的自动恢复。
  • 增加消息已读/未读状态,优化用户体验。

内容的提问来源于stack exchange,提问作者RapidCode99

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 12:13:11