You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用HttpOnly JWT令牌与Passport.js实现可扩展WebSocket方案

问题根因

  • 单实例环境下socket.userId直接存储在当前进程内存中,扩容后Socket.IO连接会因负载均衡、自动重连等被调度到不同实例,新实例无法读取旧实例内存中存储的socket.userId
  • 当前实现依赖前端主动发送REGISTER事件后才给socket绑定userId,若连接建立后还未发送REGISTER事件就发生断连,此时socket上自然没有userId属性
  • HttpOnly JWT无法被前端读取直接传递,所以不能在连接参数中直接携带userId

可行解决方案

1. 连接建立阶段直接解析HttpOnly JWT获取userId

Socket.IO的连接请求默认会自动携带同域下的所有Cookie,你可以在Socket.IO服务端增加连接中间件,直接从请求头的Cookie中解析JWT获取userId,不需要等待前端发送REGISTER事件,从根源避免socket绑定userId延迟的问题:

// Socket.IO 连接中间件
this.ioServer.use(async (socket, next) => {
  try {
    // 从socket的请求头中获取Cookie,解析出HttpOnly的JWT令牌
    const cookie = socket.handshake.headers.cookie;
    const jwtToken = cookie?.split('; ').find(row => row.startsWith('你的JWT Cookie名='))?.split('=')[1];
    if (!jwtToken) return next(new Error('未授权'));
    // 用你现有Passport JWT的校验逻辑解析令牌拿到userId
    const payload = await yourJwtVerifyFunction(jwtToken);
    // 连接阶段就把userId绑定到socket上
    socket.userId = payload.userId;
    next();
  } catch (err) {
    next(err);
  }
});

修改后你可以直接删除原有的REGISTER事件逻辑,连接成功后直接返回锁列表即可。

2. 用共享存储维护socket.id与userId的映射关系

使用Redis作为共享存储,所有实例都可以访问,每次连接成功后存储socket.id => userId的映射,断连时优先从Redis中读取userId,避免当前实例内存中没有对应数据的问题:

// 连接中间件校验通过后存储映射
this.ioServer.use(async (socket, next) => {
  // ... 上述JWT校验逻辑
  await redis.setEx(`socket:${socket.id}`, 86400, socket.userId); // 过期时间设为24小时
  next();
});

// 修改disconnect逻辑
disconnect = async socket => {
  try {
    // 优先从Redis取userId,兜底取socket上的属性
    const userId = await redis.get(`socket:${socket.id}`) || socket.userId;
    await this.deleteForUser(userId);
    // 清理Redis中的映射
    await redis.del(`socket:${socket.id}`);
    const locks = await this.pageLockService.getAllLocks();
    // 断连后要用广播通知其他用户,原逻辑socket.emit已经没用了,因为socket已经断了
    socket.broadcast.emit(UPDATE_LOCKS, { store: locks });
    socket.removeAllListeners();
  } catch (err) {
    logger.error(err);
  }
};

同时记得配置Socket.IO官方的Redis适配器,保证多实例下的广播、事件推送可以跨实例生效。

3. 增加锁过期兜底机制

为了避免极端情况下锁没有被正常释放,给每个页面锁增加过期时间:

  • 前端每30秒给持有锁的页面发送一次续期请求
  • 后台锁存储中增加expireAt字段,超过1分钟未续期的锁自动判定为失效,定时任务或者查询时自动清理失效锁
  • 额外监听Socket.IO原生的disconnect事件,不要只依赖前端主动发送的USER_DISCONNECT事件,避免用户直接关闭页面时无法触发清理逻辑:
setEvents() {
  this.ioServer.on('connection', socket => {
    // 增加原生disconnect事件监听
    socket.on('disconnect', () => this.disconnect(socket));
    socket.on(CHECK_OUT_PRODUCT, data => this.checkOutPage(data, socket));
    socket.on(CHECK_IN_PRODUCT, data => this.checkInPage(data, socket));
  });
}

内容的提问来源于stack exchange,提问作者iGeordie

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.05 08:45:03