本文详解为何直接从 html dom 中读取用户 id 并通过 socket.io 发送给服务端存在严重安全隐患,并提供基于 jwt、socket.io 认证与服务端身份绑定的安全替代方案。
在使用 Express + Socket.IO + Handlebars + JWT 的 Web 应用中,绝不可将用户 ID 渲染到前端 HTML(如 {{data}}),再由客户端 JavaScript 读取并主动发送给服务端用于数据库操作。这种做法本质上是将敏感身份标识暴露给客户端,攻击者可轻易篡改 DOM、伪造 Socket.IO 消息或重放请求,从而越权执行任意 SQL 操作(例如 UPDATE users SET money = money + 100 WHERE id = 999 —— 修改他人账户余额)。
删除所有类似以下代码:
{{data}}Handlebars 模板中无需向客户端暴露 req.user.id —— 服务端应全程持有该上下文。
利用 Express 的 req.user(JWT 解析后挂载)与 Socket.IO 的握手(handshake)机制,在连接建立时完成认证:
// app.js 或 socket setup 文件
const io = require('socket.io')(server, {
cors: { origin: '*' } // 生产环境请严格配置 origin
});
io.use((socket, next) => {
const token = socket.handshake.auth.token; // 或从 query/cookie 提取
if (!token) return next(new Error('Authentication error'));
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
socket.data.userId = decoded.id; // 绑定可信用户 ID 到 socket 实例
next();
} catch (err) {
next(new Error('Invalid token'));
}
});
io.on('connection', (socket) => {
console.log('User connected:', socket.data.userId);
// ✅ 安全示例:更新当前用户余额(无需客户端传 ID)
socket.on('addMoney', async (amount) => {
try {
const userId = socket.data.userId; // 来自服务端验证,绝对可信
await db.query('UPDATE users SET money = money + ? WHERE id = ?', [amount, userId]);
socket.emit('balanceUpdated', { success: true });
} catch (err) {
socket.emit('error', { message: 'Update failed' });
}
});
});? 前端发起请求时不再发送 userid:// client.js ✅ 安全调用(不传 ID) socket.emit('addMoney', 100);
const socket = io({
auth: {
token: localStorage.getItem('jwtToken') // 或从其他安全存储读取
}
});安全的本质不是“隐藏 ID”,而是将身份控制权完全交还服务
端。通过 Socket.IO 中间件绑定已验证用户上下文,并在业务逻辑中始终使用 socket.data.userId(而非客户端参数),即可彻底规避 ID 伪造风险。这不仅是最佳实践,更是 OWASP Top 10 中“A01:2025 – Broken Access Control”防御的核心要求。