WebSocket连接前必须在握手阶段通过URL查询参数传递Token,服务端需在upgrade事件中同步校验并拒绝非法请求,不可延至connection事件处理。
WebSocket连接前如何传递Token
websocket协议本身不支持在握手阶段像http那样携带完整的认证头(如 authorization),但浏览器发起连接时允许在 url 中附带查询参数。这是最常用、兼容性最好的token传递方式。
服务端需在升级请求的 req.url(Node.js)或 request.query(如 FastAPI)中解析该Token,而不是依赖 req.headers.authorization —— 浏览器不会自动把JWT塞进WebSocket握手的headers里。
- 前端连接写法:
new WebSocket("wss://api.example.com/ws?token=eyJhbGciOi...") - Token应放在查询参数而非路径段(避免被CDN或日志系统误截)
- 注意URL编码:若Token含
+或/,需用encodeURIComponent(token)包裹 - 不推荐用
cookie自动携带:跨域场景下默认不发送,且无法控制是否包含
服务端如何校验Token并拒绝非法连接
关键不是“等连接建立后再鉴权”,而是**在 upgrade 事件中同步校验,并决定是否调用 socket.accept()(或等价方法)**。一旦响应了101状态码,连接即成立,此时再拒绝已无意义。
以 Node.js + ws 库为例:
server.on('upgrade', async (req, socket, head) => {
const url = new URL(req.url, 'http://localhost');
const token = url.searchParams.get('token');
if (!token) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
return;
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
// 校验通过,才允许升级
server.handleUpgrade(req, socket, head, (ws) => {
ws.userId = payload.userId; // 挂载到实例上供后续使用
server.emit('connection', ws, req);
});
} catch (err) {
socket.write('HTTP/1.1 401 Invalid token\r\n\r\n');
socket.destroy();
}
});
- 必须手动写响应并
destroy()socket,否则客户端会卡在 pending 状态 - 错误响应体可为空,但状态行和空行(
\r\n\r\n)不可省略 - 不要在
connection事件里校验——那时连接已建立,只能踢人,无法阻止握手
为什么不能在 on('connection') 里做身份验证
因为 on('connection') 是WebSocket连接**已经成功建立后**触发的事件。此时TCP+HTTP升级已完成,服务端已返回101 Switching Protocols,客户端 readyState === 1。你这时发现Token无效,只能主动 ws.close(4001, 'Invalid auth'),但资源(内存、fd、心跳逻辑)已被分配,且攻击者可反复建连消耗服务端资源。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- 真正有效的防护点只有
upgrade阶段(HTTP层)或反向代理层(如Nginx用auth_request) - 某些框架(如 NestJS WebSocket)封装了守卫,但底层仍是拦截 upgrade 请求,不是监听 connection
- 如果用
express-ws,它的app.ws()也发生在 upgrade 后,同样不适用于前置鉴权
Token过期与刷新的实际处理难点
WebSocket是长连接,而JWT通常有较短有效期(如30分钟)。你无法像HTTP那样重发一个新Token给客户端——没有响应通道。所以必须设计客户端主动续期机制。
- 服务端可在连接中定期发送
{ type: "ping", expires_in: 60 }提示剩余时间 - 客户端收到后,在过期前主动断开并用新Token重建连接(先调用
fetch /auth/refresh) - 避免服务端单方面 close:有些客户端库对 close 帧处理不一致,可能静默重连失败
- 不要依赖
setInterval定期发Token——Token本身不应在WS帧中传输,易被中间人截获
真正的麻烦点在于:Token校验必须在毫秒级完成,且不能阻塞upgrade事件;而数据库查用户、Redis验黑名单这些IO操作容易超时。建议只做无状态校验(如JWT签名+exp),用户权限等后续消息层面再查。










