1007 字
5 分钟
Socket.io 中间件机制与错误事件传递详解
2025-07-17
无标签

中间件机制(Middleware)#

什么是 Socket.io 中间件#

Socket.io 中间件是在客户端连接建立之前执行的函数,用于验证、授权或预处理连接请求。

中间件的执行时机#

客户端发起连接 → 中间件验证 → 连接建立/拒绝 → 触发相应事件

基本语法结构#

io.use((socket, next) => {
// 验证逻辑
if (验证通过) {
next(); // 继续连接流程
} else {
next(new Error("错误信息")); // 拒绝连接并传递错误
}
});

中间件参数详解#

  • socket: 即将建立的 socket 连接对象,包含 handshake 信息
  • next: 回调函数,控制中间件流程的继续或中断

错误事件传递机制#

服务端错误传递#

// ✅ 正确方式:在中间件中传递错误
io.use((socket, next) => {
try {
const token = socket.handshake.auth.token;
if (!token) {
// 通过 next 传递错误到客户端
return next(new Error("Authentication error: No token provided"));
}
jwt.verify(token, SECRET_KEY);
next(); // 验证通过,继续连接
} catch (err) {
// JWT验证失败
next(new Error("Authentication error: Invalid token"));
}
});

客户端错误接收#

const socket = io({
auth: {
token: "your_token_here",
},
});
// 连接成功事件
socket.on("connect", () => {
console.log("✅ 连接成功");
});
// 连接错误事件 - 接收中间件传递的错误
socket.on("connect_error", (error) => {
console.log("❌ 连接失败:", error.message);
// error.message 包含服务端 next(new Error()) 中的错误信息
});

对比:正确 vs 错误的实现方式#

❌ 错误方式:连接后断开#

// 问题:连接已建立,disconnect 不会触发 connect_error
io.on("connection", (socket) => {
const token = socket.handshake.auth.token;
if (!token) {
socket.disconnect(true); // 客户端不会收到 connect_error
return;
}
});

问题分析

  • 连接已成功建立
  • socket.disconnect() 只是断开连接,不是连接失败
  • 客户端先收到 connect 事件,然后收到 disconnect 事件
  • 客户端的 connect_error 事件永远不会触发

✅ 正确方式:中间件验证#

// 解决方案:在连接建立前验证
io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (!token) {
next(new Error("No token provided")); // 正确触发 connect_error
return;
}
next(); // 验证通过
});

优势

  • 连接根本不会建立
  • 客户端直接收到 connect_error 事件
  • 错误信息清晰传递
  • 资源使用更高效

中间件的执行流程#

多个中间件的执行顺序#

// 中间件1:基础验证
io.use((socket, next) => {
console.log("中间件1:基础验证");
next();
});
// 中间件2:Token验证
io.use((socket, next) => {
console.log("中间件2:Token验证");
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error("Missing token"));
}
next();
});
// 中间件3:权限检查
io.use((socket, next) => {
console.log("中间件3:权限检查");
// 其他验证逻辑...
next();
});

执行顺序:按注册顺序依次执行,任何一个中间件调用 next(error) 都会中断流程。

中间件中的数据传递#

io.use((socket, next) => {
try {
const decoded = jwt.verify(token, SECRET_KEY);
// 将解码后的用户信息挂载到 socket 对象
socket.user = decoded;
next();
} catch (err) {
next(new Error("Invalid token"));
}
});
// 在连接处理器中可以访问 socket.user
io.on("connection", (socket) => {
console.log("用户信息:", socket.user); // 可以访问中间件中设置的数据
});

实际应用场景#

1. JWT Token 验证#

io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error("Authentication required"));
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
socket.userId = decoded.userId;
socket.userRole = decoded.role;
next();
} catch (err) {
next(new Error("Invalid authentication token"));
}
});

2. IP 白名单验证#

io.use((socket, next) => {
const clientIP = socket.handshake.address;
const allowedIPs = ["127.0.0.1", "192.168.1.0/24"];
if (!isIPAllowed(clientIP, allowedIPs)) {
return next(new Error("IP address not allowed"));
}
next();
});

3. 连接频率限制#

const connectionAttempts = new Map();
io.use((socket, next) => {
const clientIP = socket.handshake.address;
const now = Date.now();
const attempts = connectionAttempts.get(clientIP) || [];
// 清理1分钟前的记录
const recentAttempts = attempts.filter((time) => now - time < 60000);
if (recentAttempts.length >= 10) {
return next(new Error("Too many connection attempts"));
}
recentAttempts.push(now);
connectionAttempts.set(clientIP, recentAttempts);
next();
});

最佳实践总结#

  1. 始终使用中间件进行连接前验证
  2. 通过 next(new Error()) 传递具体的错误信息
  3. 在中间件中设置 socket 属性,供后续使用
  4. 保持中间件逻辑简洁和高效
  5. 合理组织多个中间件的执行顺序
  6. 在客户端正确处理 connect_error 事件

这样的机制确保了 Socket.io 应用的安全性和用户体验的一致性。

Socket.io 中间件机制与错误事件传递详解
https://blog.cuixu.cn/posts/websocket/socketio-middleware/
作者
崔旭
发布于
2025-07-17
许可协议
CC BY-NC-SA 4.0