WebSocket com autenticação JWT em Node.js
WebSockets são conexões persistentes bidirecionais — o servidor pode enviar mensagens ao cliente a qualquer momento. Autenticar essas conexões é diferente de requisições HTTP: o token precisa ser validado no handshake inicial, e a conexão deve ser encerrada se o token expirar durante a sessão.
WebSocket autenticado com biblioteca ws
Validar JWT no handshake HTTP antes de fazer o upgrade para WebSocket:
// npm install ws @types/ws
import { WebSocketServer, WebSocket } from 'ws'
import { IncomingMessage } from 'http'
import { verificarAccessToken, type TokenPayload } from './lib/jwt'
// Estender WebSocket para guardar dados do usuário:
interface WebSocketAutenticado extends WebSocket {
usuario?: TokenPayload
isAlive?: boolean
}
const wss = new WebSocketServer({ port: 8080 })
// Autenticar no handshake:
wss.on('connection', async (ws: WebSocketAutenticado, req: IncomingMessage) => {
// Token pode vir como query param (impossível usar header em WebSocket browser nativo):
// ws://api.seudominio.com.br/ws?token=eyJ...
const url = new URL(req.url!, `http://${req.headers.host}`)
const token = url.searchParams.get('token')
if (!token) {
ws.close(4001, 'Token não fornecido')
return
}
try {
ws.usuario = await verificarAccessToken(token)
} catch {
ws.close(4001, 'Token inválido ou expirado')
return
}
console.log(`WebSocket conectado: ${ws.usuario.email}`)
ws.isAlive = true
ws.on('pong', () => { ws.isAlive = true })
ws.on('message', (data) => {
const msg = JSON.parse(data.toString())
handleMensagem(ws, msg)
})
ws.on('close', () => {
console.log(`WebSocket desconectado: ${ws.usuario?.email}`)
})
// Confirmar conexão:
ws.send(JSON.stringify({ type: 'connected', userId: ws.usuario.userId }))
})
// Heartbeat para detectar conexões mortas:
const heartbeat = setInterval(() => {
wss.clients.forEach((ws: WebSocketAutenticado) => {
if (!ws.isAlive) return ws.terminate()
ws.isAlive = false
ws.ping()
})
}, 30_000)
wss.on('close', () => clearInterval(heartbeat))Socket.IO com autenticação JWT
Middleware de autenticação e rooms por usuário no Socket.IO:
// npm install socket.io
import { Server } from 'socket.io'
import { verificarAccessToken } from './lib/jwt'
const io = new Server(httpServer, {
cors: {
origin: ['https://seudominio.com.br'],
credentials: true,
},
})
// Middleware de autenticação (executa antes de cada conexão):
io.use(async (socket, next) => {
const token =
socket.handshake.auth.token || // enviado pelo cliente via auth option
socket.handshake.headers.authorization?.slice(7)
if (!token) return next(new Error('Token não fornecido'))
try {
socket.data.usuario = await verificarAccessToken(token)
next()
} catch {
next(new Error('Token inválido'))
}
})
io.on('connection', (socket) => {
const usuario = socket.data.usuario
// Entrar em rooms pessoais (receber notificações diretas):
socket.join(`user:${usuario.userId}`)
// Entrar em rooms de organização:
socket.join(`org:${usuario.orgId}`)
console.log(`Socket conectado: ${usuario.email} [${socket.id}]`)
// Escutar eventos do cliente:
socket.on('mensagem:enviar', async (data) => {
// Emitir para todos na room:
io.to(`org:${usuario.orgId}`).emit('mensagem:nova', {
de: usuario.email,
conteudo: data.conteudo,
timestamp: Date.now(),
})
})
socket.on('disconnect', () => {
console.log(`Socket desconectado: ${usuario.email}`)
})
})
// Enviar notificação para usuário específico do servidor:
export function notificarUsuario(userId: string, evento: string, dados: unknown) {
io.to(`user:${userId}`).emit(evento, dados)
}Cliente frontend com reconnect e refresh de token
Reconectar automaticamente com token renovado ao receber erro 4001:
// frontend/websocket.ts
import { refreshAccessToken } from './auth'
let ws: WebSocket | null = null
let token = ''
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
let reconnectDelay = 1000
export async function conectarWebSocket() {
token = getAccessTokenFromMemoria()
ws = new WebSocket(`wss://api.seudominio.com.br/ws?token=${token}`)
ws.addEventListener('open', () => {
console.log('WebSocket conectado')
reconnectDelay = 1000 // resetar delay ao conectar com sucesso
})
ws.addEventListener('message', (event) => {
const msg = JSON.parse(event.data)
handleMensagem(msg)
})
ws.addEventListener('close', async (event) => {
console.log(`WebSocket fechado: ${event.code} ${event.reason}`)
if (event.code === 4001) {
// Token expirado — tentar renovar:
try {
token = await refreshAccessToken()
salvarAccessTokenNaMemoria(token)
conectarWebSocket() // reconectar imediatamente com novo token
return
} catch {
// Refresh falhou — redirecionar para login:
window.location.href = '/login'
return
}
}
// Reconectar com backoff exponencial:
if (reconnectTimer) clearTimeout(reconnectTimer)
reconnectTimer = setTimeout(() => {
reconnectDelay = Math.min(reconnectDelay * 2, 30_000)
conectarWebSocket()
}, reconnectDelay)
})
ws.addEventListener('error', (error) => {
console.error('WebSocket erro:', error)
})
return ws
}
export function enviarMensagem(tipo: string, dados: unknown) {
if (ws?.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: tipo, ...dados as object }))
}
}Broadcast e mensagens direcionadas
Padrões para enviar mensagens para usuários, grupos e todos:
// Padrões de envio no Socket.IO:
// 1. Para um usuário específico (de qualquer parte do código):
io.to(`user:${userId}`).emit('notificacao', { mensagem: 'Novo pedido criado!' })
// 2. Para todos em uma organização:
io.to(`org:${orgId}`).emit('pedido:atualizado', { pedidoId, status: 'aprovado' })
// 3. Broadcast para todos (exceto o remetente):
socket.broadcast.emit('usuario:online', { nome: usuario.nome })
// 4. Para todos (incluindo o remetente):
io.emit('manutencao:aviso', { mensagem: 'Sistema em manutenção em 10 min' })
// 5. Para uma sala específica (ex: chat de suporte):
io.to(`ticket:${ticketId}`).emit('mensagem:nova', dados)
// Usando com Express — enviar notificação em rota HTTP:
app.post('/api/pedidos', autenticar, async (req, res) => {
const pedido = await criarPedido(req.body)
// Notificar o criador do pedido em tempo real:
notificarUsuario(req.usuario!.userId, 'pedido:criado', { pedidoId: pedido.id })
// Notificar administradores:
io.to('role:admin').emit('pedido:novo', { pedido })
res.status(201).json(pedido)
})WebSocket atrás de Nginx
Configurar proxy WebSocket no Nginx:
# /etc/nginx/sites-enabled/api.conf
server {
listen 443 ssl;
server_name api.seudominio.com.br;
# Proxy para API HTTP:
location /api/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Proxy WebSocket (upgrade necessário):
location /ws {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
# Headers obrigatórios para WebSocket:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Timeout longo para conexões persistentes:
proxy_read_timeout 3600s; # 1 hora
proxy_send_timeout 3600s;
# Desabilitar buffering para WebSocket:
proxy_buffering off;
}
# Socket.IO usa /socket.io/ por padrão:
location /socket.io/ {
proxy_pass http://127.0.0.1:3000/socket.io/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
proxy_buffering off;
}
}$ runstack deploy --plan starter
Não quer configurar manualmente?
Não quer configurar manualmente? Implante o VPS para APIs em menos de 3 minutos com a Runstack. Infraestrutura da OPEN DATACENTER, com servidores no Brasil.
Perguntas frequentes
Conteúdos relacionados