中间件机制(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();
});
最佳实践总结
- 始终使用中间件进行连接前验证
- 通过
next(new Error())传递具体的错误信息 - 在中间件中设置 socket 属性,供后续使用
- 保持中间件逻辑简洁和高效
- 合理组织多个中间件的执行顺序
- 在客户端正确处理
connect_error事件
这样的机制确保了 Socket.io 应用的安全性和用户体验的一致性。