如何安全地在 Socket.IO 中传递用户 ID(避免客户端篡改风险)

本文详解为何直接从 html dom 提取用户 id 并通过 socket.io 发送给服务器存在严重安全隐患,并提供基于 jwt 和服务端身份绑定的可靠替代方案。

在当前实现中,你将 req.user.id 渲染到 Handlebars 模板中:

{{data}}

再通过前端 JavaScript 读取该值并发送给服务端:

let userid = document.getElementById('iduser').innerHTML;
socket.emit('updateMoney', { userId: userid, amount: 100 });

这是不安全的——且完全不可接受用于生产环境。
原因在于:任何用户均可轻松修改 DOM 中的 #iduser 内容(例如通过浏览器开发者工具),伪造任意 userId,从而执行越权操作(如篡改他人账户余额)。 即便页面受 JWT 登录保护,一旦敏感标识(如 id)暴露于客户端并被信任为“权威来源”,整个认证逻辑即被绕过。

✅ 正确做法:服务端自主识别用户身份,拒绝客户端传入用户 ID

Socket.IO 连接应与已认证的 HTTP 会话关联,而非依赖客户端提交的 ID。以下是推荐的安全实践:

1. 在 Socket.IO 连接时绑定用户身份(推荐)

利用 Express 的 session 或 JWT,在握手阶段验证并挂载用户信息:

// app.js —— 初始化 Socket.IO 时注入用户上下文
const io = new Server(server, {
  cors: { origin: "http://localhost:3000", credentials: true }
});

io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  if (!token) return next(new Error("Authentication error: missing token"));

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    socket.data.userId = decoded.id; // ✅ 服务端可信身份
    next();
  } catch (err) {
    next(new Error("Invalid token"));
  }
});

io.on("connection", (socket) => {
  console.log("User connected:", socket.data.userId);

  // 客户端只需发业务请求,无需传 userId
  socket.on("updateMoney", async ({ amount }) => {
    const userId = socket.data.userId; // ✅ 来自服务端验证,不可伪造
    try {
      await db.query('UPDATE users SET money = money + ? WHERE id = ?', [amount, userId]);
      socket.emit("moneyUpdated", { success: true });
    } catch (err) {
      socket.emit("error", { message: "Update failed" });
    }
  });
});

2. 前端发起请求时仅携带 token(不传 ID)

// client.js —— 连接时附带 JWT
const token = localStorage.getItem('jwtToken'); // 从登录后存储的 token 获取
const socket = io({
  auth: { token }
});

// 发送业务事件(无 userId)
document.getElementById('add100Btn').addEventListener('click', () => {
  socket.emit('updateMoney', { amount: 100 });
});

3. 后端渲染时无需暴露用户 ID(可选优化)

若非必要,建议避免在 HTML 中渲染 iduser:


{{data}}


Hello, {{req.user.name}}

⚠️ 关键注意事项

  • 永远不要在 SQL 查询中直接使用 document.getElementById(...).innerHTML 的值 —— 这属于典型的“未经验证的用户输入”,极易导致 SQL 注入或水平越权。
  • JWT 必须设置合理过期时间(如 expiresIn: '24h'),且密钥(JWT_SECRET)需严格保密、不可硬编码在前端。
  • Socket.IO 连接应启用 credentials: true 并配置 CORS 白名单,防止跨站恶意连接。
  • 所有数据库操作务必使用参数化查询(你已正确使用 ? 占位符,这点值得肯定)。

✅ 总结

你当前的方法不具备基本安全性,不应投入公共项目。真正的安全不是“隐藏 ID”,而是剥离客户端对身份标识的控制权:让服务端在每次通信起点(HTTP 请求或 Socket 连接握手)完成身份核验,并将可信用户上下文(如 socket.data.userId)贯穿后续所有操作。这样,即使攻击者篡改前端代码,也无法影响服务端的身份判定逻辑。

遵循此模式,你的 UPDATE users SET money = money + ? WHERE id = ? 查询才能真正运行在可信上下文中,兼顾功能性与安全性。