本文详解为何直接从 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)暴露于客户端并被信任为“权威来源”,整个认证逻辑即被绕过。
Socket.IO 连接应与已认证的 HTTP 会话关联,而非依赖客户端提交的 ID。以下是推荐的安全实践:
利用 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" });
}
});
});// 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 });
});若非必要,建议避免在 HTML 中渲染 iduser:
{{data}}
Hello, {{req.user.name}}你当前的方法不具备基本安全性,不应投入公共项目。真正的安全不是“隐藏 ID”,而是剥离客户端对身份标识的控制权:让服务端在每次通信起点(HTTP 请求或 Socket 连接握手)完成身份核验,并将可信用户上下文(如 socket.data.userId)贯穿后续所有操作。这样,即使攻击者篡改前端代码,也无法影响服务端的身份判定逻辑。
遵循此模式,你的 UPDATE users SET money = money + ? WHERE id = ? 查询才能真正运行在可信上下文中,兼顾功能性与安全性。