如何使用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
相关产品推荐
相关产品推荐

