中间件机制(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 应用的安全性和用户体验的一致性。