WebSocket 安全实践 #

安全威胁概述 #

text
┌─────────────────────────────────────────────────────────────┐
│                    WebSocket 安全威胁                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 认证绕过                                                │
│     ─────────────────────────────────────────────────────   │
│     未授权用户建立 WebSocket 连接                           │
│     冒充合法用户身份                                        │
│                                                             │
│  2. 跨站 WebSocket 劫持(CSWSH)                            │
│     ─────────────────────────────────────────────────────   │
│     恶意网站发起 WebSocket 连接                             │
│     利用用户已登录状态                                      │
│                                                             │
│  3. 注入攻击                                                │
│     ─────────────────────────────────────────────────────   │
│     XSS(跨站脚本攻击)                                     │
│     SQL 注入                                                │
│     命令注入                                                │
│                                                             │
│  4. 拒绝服务攻击(DoS/DDoS)                                │
│     ─────────────────────────────────────────────────────   │
│     大量连接耗尽服务器资源                                  │
│     发送超大消息                                            │
│     频繁建立断开连接                                        │
│                                                             │
│  5. 中间人攻击                                              │
│     ─────────────────────────────────────────────────────   │
│     窃听通信内容                                            │
│     篡改消息                                                │
│                                                             │
│  6. 数据泄露                                                │
│     ─────────────────────────────────────────────────────   │
│     敏感信息暴露                                            │
│     日志泄露                                                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

身份认证 #

Token 认证 #

javascript
const WebSocket = require('ws');
const jwt = require('jwt');

const wss = new WebSocket.Server({
  port: 8080,
  verifyClient: (info, callback) => {
    const token = extractToken(info);
    
    if (!token) {
      return callback(false, 401, '未提供认证令牌');
    }
    
    try {
      const decoded = jwt.verify(token, process.env.JWT_SECRET);
      info.req.user = decoded;
      callback(true);
    } catch (error) {
      callback(false, 401, '无效的认证令牌');
    }
  }
});

function extractToken(info) {
  const url = new URL(info.req.url, `http://${info.req.headers.host}`);
  
  let token = url.searchParams.get('token');
  
  if (!token) {
    const authHeader = info.req.headers.authorization;
    if (authHeader && authHeader.startsWith('Bearer ')) {
      token = authHeader.substring(7);
    }
  }
  
  if (!token) {
    const protocols = info.req.headers['sec-websocket-protocol'];
    if (protocols) {
      const protocolList = protocols.split(', ');
      token = protocolList.find(p => p.startsWith('token.'));
      if (token) {
        token = token.substring(6);
      }
    }
  }
  
  return token;
}

wss.on('connection', (ws, request) => {
  const user = request.user;
  ws.userId = user.id;
  
  ws.on('message', (data) => {
    console.log(`用户 ${user.id} 发送消息:`, data.toString());
  });
});

首条消息认证 #

javascript
const WebSocket = require('ws');
const jwt = require('jsonwebtoken');

const wss = new WebSocket.Server({ port: 8080 });

const unauthenticatedClients = new Set();
const authenticatedClients = new Map();

const AUTH_TIMEOUT = 30000;

wss.on('connection', (ws) => {
  unauthenticatedClients.add(ws);
  ws.authenticated = false;
  
  const authTimeout = setTimeout(() => {
    if (!ws.authenticated) {
      ws.close(4001, '认证超时');
      unauthenticatedClients.delete(ws);
    }
  }, AUTH_TIMEOUT);
  
  ws.on('message', (data) => {
    if (!ws.authenticated) {
      try {
        const message = JSON.parse(data);
        
        if (message.type === 'auth') {
          const decoded = jwt.verify(message.token, process.env.JWT_SECRET);
          
          ws.authenticated = true;
          ws.userId = decoded.id;
          ws.user = decoded;
          
          clearTimeout(authTimeout);
          unauthenticatedClients.delete(ws);
          authenticatedClients.set(ws.userId, ws);
          
          ws.send(JSON.stringify({
            type: 'auth_success',
            message: '认证成功'
          }));
        } else {
          ws.close(4002, '请先完成认证');
        }
      } catch (error) {
        ws.close(4003, '认证失败');
      }
      
      return;
    }
    
    handleMessage(ws, data);
  });
  
  ws.on('close', () => {
    unauthenticatedClients.delete(ws);
    if (ws.userId) {
      authenticatedClients.delete(ws.userId);
    }
  });
});

function handleMessage(ws, data) {
  console.log(`用户 ${ws.userId} 发送消息:`, data.toString());
}

OAuth 2.0 集成 #

javascript
const WebSocket = require('ws');
const axios = require('axios');

class OAuthWebSocketServer {
  constructor(port, oauthConfig) {
    this.wss = new WebSocket.Server({ port });
    this.oauthConfig = oauthConfig;
    
    this.wss.on('connection', (ws, request) => {
      this.handleConnection(ws, request);
    });
  }
  
  async handleConnection(ws, request) {
    const token = this.extractToken(request);
    
    if (!token) {
      ws.close(4001, '未提供访问令牌');
      return;
    }
    
    try {
      const userInfo = await this.verifyToken(token);
      
      ws.user = userInfo;
      ws.authenticated = true;
      
      ws.send(JSON.stringify({
        type: 'auth_success',
        user: userInfo
      }));
      
      ws.on('message', (data) => {
        this.handleMessage(ws, data);
      });
    } catch (error) {
      ws.close(4003, '令牌验证失败');
    }
  }
  
  async verifyToken(token) {
    const response = await axios.get(this.oauthConfig.userInfoUrl, {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    });
    
    return response.data;
  }
  
  extractToken(request) {
    const url = new URL(request.url, `http://${request.headers.host}`);
    return url.searchParams.get('access_token');
  }
  
  handleMessage(ws, data) {
    console.log(`用户 ${ws.user.id} 发送消息:`, data.toString());
  }
}

const server = new OAuthWebSocketServer(8080, {
  userInfoUrl: 'https://oauth.example.com/userinfo'
});

授权机制 #

基于角色的访问控制 #

javascript
class AuthorizationManager {
  constructor() {
    this.roles = new Map();
    this.permissions = new Map();
    
    this.initRoles();
  }
  
  initRoles() {
    this.roles.set('admin', ['read', 'write', 'delete', 'manage']);
    this.roles.set('user', ['read', 'write']);
    this.roles.set('guest', ['read']);
  }
  
  hasPermission(user, permission) {
    const role = user.role || 'guest';
    const permissions = this.roles.get(role) || [];
    return permissions.includes(permission);
  }
  
  canAccessRoom(user, room) {
    if (room.startsWith('admin-')) {
      return user.role === 'admin';
    }
    
    if (room.startsWith('private-')) {
      return room.includes(user.id);
    }
    
    return true;
  }
  
  canSendMessage(user, messageType) {
    const restrictions = {
      'broadcast': ['admin', 'moderator'],
      'announcement': ['admin'],
      'file': ['admin', 'user'],
      'text': ['admin', 'user', 'guest']
    };
    
    const allowedRoles = restrictions[messageType] || [];
    return allowedRoles.includes(user.role);
  }
}

const authManager = new AuthorizationManager();

function handleWebSocketMessage(ws, data) {
  const message = JSON.parse(data);
  
  switch (message.type) {
    case 'join_room':
      if (!authManager.canAccessRoom(ws.user, message.room)) {
        ws.send(JSON.stringify({
          type: 'error',
          message: '无权访问该房间'
        }));
        return;
      }
      ws.room = message.room;
      break;
      
    case 'broadcast':
      if (!authManager.canSendMessage(ws.user, 'broadcast')) {
        ws.send(JSON.stringify({
          type: 'error',
          message: '无权发送广播消息'
        }));
        return;
      }
      broadcast(message.content);
      break;
  }
}

资源级权限控制 #

javascript
class ResourceAuthorization {
  constructor() {
    this.resourcePermissions = new Map();
  }
  
  canAccess(user, resource, action) {
    const key = `${user.id}:${resource.type}:${resource.id}`;
    const permissions = this.resourcePermissions.get(key) || [];
    
    return permissions.includes(action);
  }
  
  grantPermission(userId, resource, actions) {
    const key = `${userId}:${resource.type}:${resource.id}`;
    const existing = this.resourcePermissions.get(key) || [];
    
    const updated = [...new Set([...existing, ...actions])];
    this.resourcePermissions.set(key, updated);
  }
  
  revokePermission(userId, resource, actions) {
    const key = `${userId}:${resource.type}:${resource.id}`;
    const existing = this.resourcePermissions.get(key) || [];
    
    const updated = existing.filter(a => !actions.includes(a));
    
    if (updated.length > 0) {
      this.resourcePermissions.set(key, updated);
    } else {
      this.resourcePermissions.delete(key);
    }
  }
}

const resourceAuth = new ResourceAuthorization();

resourceAuth.grantPermission('user-123', { type: 'document', id: 'doc-456' }, ['read', 'write']);

function handleDocumentAccess(ws, message) {
  const { documentId, action } = message;
  
  if (!resourceAuth.canAccess(ws.user, { type: 'document', id: documentId }, action)) {
    ws.send(JSON.stringify({
      type: 'error',
      message: '无权访问该文档'
    }));
    return;
  }
  
  processDocumentAccess(ws, documentId, action);
}

跨域安全 #

Origin 验证 #

javascript
const WebSocket = require('ws');

const allowedOrigins = [
  'https://example.com',
  'https://app.example.com',
  'https://localhost:3000'
];

const wss = new WebSocket.Server({
  port: 8080,
  verifyClient: (info, callback) => {
    const origin = info.origin || info.req.headers.origin;
    
    if (!origin) {
      console.warn('缺少 Origin 头部');
      return callback(false, 403, '缺少 Origin 头部');
    }
    
    if (!allowedOrigins.includes(origin)) {
      console.warn(`拒绝来自 ${origin} 的连接`);
      return callback(false, 403, '不允许的来源');
    }
    
    callback(true);
  }
});

wss.on('connection', (ws, request) => {
  console.log('新连接,来源:', request.headers.origin);
});

动态 Origin 验证 #

javascript
const WebSocket = require('ws');

class OriginValidator {
  constructor() {
    this.allowedPatterns = [
      /^https:\/\/[\w-]+\.example\.com$/,
      /^https:\/\/localhost:\d+$/
    ];
    this.blockedOrigins = new Set();
  }
  
  isValid(origin) {
    if (!origin) {
      return false;
    }
    
    if (this.blockedOrigins.has(origin)) {
      return false;
    }
    
    return this.allowedPatterns.some(pattern => pattern.test(origin));
  }
  
  blockOrigin(origin) {
    this.blockedOrigins.add(origin);
  }
  
  unblockOrigin(origin) {
    this.blockedOrigins.delete(origin);
  }
}

const originValidator = new OriginValidator();

const wss = new WebSocket.Server({
  port: 8080,
  verifyClient: (info, callback) => {
    const origin = info.origin || info.req.headers.origin;
    
    if (!originValidator.isValid(origin)) {
      return callback(false, 403, '不允许的来源');
    }
    
    callback(true);
  }
});

输入验证 #

消息验证 #

javascript
const Joi = require('joi');

const messageSchema = Joi.object({
  type: Joi.string()
    .valid('message', 'join', 'leave', 'ping')
    .required(),
  payload: Joi.object().required(),
  timestamp: Joi.number().integer().positive(),
  id: Joi.string().max(64)
});

const chatMessageSchema = Joi.object({
  type: Joi.string().valid('message').required(),
  payload: Joi.object({
    room: Joi.string().max(64).required(),
    content: Joi.string().max(10000).required()
  }).required()
});

function validateMessage(data) {
  try {
    const message = JSON.parse(data);
    
    const { error, value } = messageSchema.validate(message);
    
    if (error) {
      return { valid: false, error: error.details[0].message };
    }
    
    if (message.type === 'message') {
      const { error: chatError } = chatMessageSchema.validate(message);
      if (chatError) {
        return { valid: false, error: chatError.details[0].message };
      }
    }
    
    return { valid: true, value };
  } catch (e) {
    return { valid: false, error: '无效的 JSON 格式' };
  }
}

wss.on('connection', (ws) => {
  ws.on('message', (data) => {
    const result = validateMessage(data);
    
    if (!result.valid) {
      ws.send(JSON.stringify({
        type: 'error',
        message: result.error
      }));
      return;
    }
    
    handleMessage(ws, result.value);
  });
});

XSS 防护 #

javascript
const validator = require('validator');

function sanitizeMessage(message) {
  if (typeof message === 'string') {
    return validator.escape(message);
  }
  
  if (typeof message === 'object' && message !== null) {
    const sanitized = {};
    
    for (const [key, value] of Object.entries(message)) {
      if (typeof value === 'string') {
        sanitized[key] = validator.escape(value);
      } else if (typeof value === 'object') {
        sanitized[key] = sanitizeMessage(value);
      } else {
        sanitized[key] = value;
      }
    }
    
    return sanitized;
  }
  
  return message;
}

function sanitizeHTML(html) {
  const allowedTags = ['b', 'i', 'u', 'strong', 'em', 'br'];
  return validator.stripLow(validator.escape(html));
}

wss.on('connection', (ws) => {
  ws.on('message', (data) => {
    try {
      const message = JSON.parse(data);
      const sanitized = sanitizeMessage(message);
      
      handleMessage(ws, sanitized);
    } catch (error) {
      ws.send(JSON.stringify({
        type: 'error',
        message: '消息处理失败'
      }));
    }
  });
});

速率限制 #

连接速率限制 #

javascript
const WebSocket = require('ws');

class ConnectionRateLimiter {
  constructor(options = {}) {
    this.windowMs = options.windowMs || 60000;
    this.maxConnections = options.maxConnections || 100;
    this.connections = new Map();
  }
  
  check(clientId) {
    const now = Date.now();
    const windowStart = now - this.windowMs;
    
    if (!this.connections.has(clientId)) {
      this.connections.set(clientId, []);
    }
    
    const connectionTimes = this.connections.get(clientId);
    
    const recentConnections = connectionTimes.filter(time => time > windowStart);
    
    if (recentConnections.length >= this.maxConnections) {
      return {
        allowed: false,
        retryAfter: recentConnections[0] + this.windowMs - now
      };
    }
    
    recentConnections.push(now);
    this.connections.set(clientId, recentConnections);
    
    return { allowed: true };
  }
  
  cleanup() {
    const now = Date.now();
    const windowStart = now - this.windowMs;
    
    this.connections.forEach((times, clientId) => {
      const recent = times.filter(time => time > windowStart);
      if (recent.length === 0) {
        this.connections.delete(clientId);
      } else {
        this.connections.set(clientId, recent);
      }
    });
  }
}

const rateLimiter = new ConnectionRateLimiter({
  windowMs: 60000,
  maxConnections: 10
});

setInterval(() => rateLimiter.cleanup(), 60000);

const wss = new WebSocket.Server({
  port: 8080,
  verifyClient: (info, callback) => {
    const clientIp = info.req.socket.remoteAddress;
    
    const result = rateLimiter.check(clientIp);
    
    if (!result.allowed) {
      return callback(false, 429, '连接过于频繁,请稍后再试');
    }
    
    callback(true);
  }
});

消息速率限制 #

javascript
class MessageRateLimiter {
  constructor(options = {}) {
    this.windowMs = options.windowMs || 1000;
    this.maxMessages = options.maxMessages || 10;
    this.clients = new Map();
  }
  
  check(clientId) {
    const now = Date.now();
    
    if (!this.clients.has(clientId)) {
      this.clients.set(clientId, {
        count: 0,
        windowStart: now
      });
    }
    
    const client = this.clients.get(clientId);
    
    if (now - client.windowStart > this.windowMs) {
      client.count = 0;
      client.windowStart = now;
    }
    
    if (client.count >= this.maxMessages) {
      return {
        allowed: false,
        retryAfter: client.windowStart + this.windowMs - now
      };
    }
    
    client.count++;
    return { allowed: true };
  }
  
  reset(clientId) {
    this.clients.delete(clientId);
  }
}

const messageLimiter = new MessageRateLimiter({
  windowMs: 1000,
  maxMessages: 5
});

wss.on('connection', (ws) => {
  ws.on('message', (data) => {
    const result = messageLimiter.check(ws.userId || ws.id);
    
    if (!result.allowed) {
      ws.send(JSON.stringify({
        type: 'error',
        message: '消息发送过于频繁',
        retryAfter: result.retryAfter
      }));
      return;
    }
    
    handleMessage(ws, data);
  });
  
  ws.on('close', () => {
    messageLimiter.reset(ws.userId || ws.id);
  });
});

DDoS 防护 #

连接限制 #

javascript
class DDoSProtector {
  constructor(options = {}) {
    this.maxConnectionsPerIp = options.maxConnectionsPerIp || 50;
    this.maxTotalConnections = options.maxTotalConnections || 10000;
    this.connectionTimeout = options.connectionTimeout || 30000;
    
    this.ipConnections = new Map();
    this.totalConnections = 0;
    this.suspiciousIps = new Set();
  }
  
  canConnect(ip) {
    if (this.suspiciousIps.has(ip)) {
      return { allowed: false, reason: 'IP 被列入黑名单' };
    }
    
    if (this.totalConnections >= this.maxTotalConnections) {
      return { allowed: false, reason: '服务器连接数已满' };
    }
    
    const ipCount = this.ipConnections.get(ip) || 0;
    
    if (ipCount >= this.maxConnectionsPerIp) {
      this.suspiciousIps.add(ip);
      return { allowed: false, reason: 'IP 连接数超限' };
    }
    
    return { allowed: true };
  }
  
  addConnection(ip) {
    const count = this.ipConnections.get(ip) || 0;
    this.ipConnections.set(ip, count + 1);
    this.totalConnections++;
  }
  
  removeConnection(ip) {
    const count = this.ipConnections.get(ip) || 0;
    
    if (count > 1) {
      this.ipConnections.set(ip, count - 1);
    } else {
      this.ipConnections.delete(ip);
    }
    
    this.totalConnections--;
  }
  
  blockIp(ip) {
    this.suspiciousIps.add(ip);
  }
  
  unblockIp(ip) {
    this.suspiciousIps.delete(ip);
  }
}

const ddosProtector = new DDoSProtector();

const wss = new WebSocket.Server({
  port: 8080,
  verifyClient: (info, callback) => {
    const ip = info.req.socket.remoteAddress;
    
    const result = ddosProtector.canConnect(ip);
    
    if (!result.allowed) {
      console.warn(`拒绝连接: ${ip}, 原因: ${result.reason}`);
      return callback(false, 429, result.reason);
    }
    
    callback(true);
  }
});

wss.on('connection', (ws, request) => {
  const ip = request.socket.remoteAddress;
  ddosProtector.addConnection(ip);
  
  ws.on('close', () => {
    ddosProtector.removeConnection(ip);
  });
});

消息大小限制 #

javascript
const WebSocket = require('ws');

const wss = new WebSocket.Server({
  port: 8080,
  maxPayload: 1024 * 1024
});

class MessageSizeValidator {
  constructor(options = {}) {
    this.maxSize = options.maxSize || 64 * 1024;
    this.maxBinarySize = options.maxBinarySize || 1024 * 1024;
  }
  
  validate(data, isBinary) {
    const size = Buffer.byteLength(data);
    const limit = isBinary ? this.maxBinarySize : this.maxSize;
    
    if (size > limit) {
      return {
        valid: false,
        size,
        limit,
        message: `消息大小 ${size} 超过限制 ${limit}`
      };
    }
    
    return { valid: true, size };
  }
}

const sizeValidator = new MessageSizeValidator();

wss.on('connection', (ws) => {
  ws.on('message', (data, isBinary) => {
    const result = sizeValidator.validate(data, isBinary);
    
    if (!result.valid) {
      console.warn(`消息大小超限: ${result.message}`);
      ws.send(JSON.stringify({
        type: 'error',
        message: '消息大小超过限制'
      }));
      return;
    }
    
    handleMessage(ws, data, isBinary);
  });
});

加密传输 #

强制 WSS #

javascript
const https = require('https');
const fs = require('fs');
const WebSocket = require('ws');

const server = https.createServer({
  cert: fs.readFileSync('/path/to/cert.pem'),
  key: fs.readFileSync('/path/to/key.pem')
});

const wss = new WebSocket.Server({ server });

wss.on('connection', (ws) => {
  console.log('安全 WebSocket 连接');
});

server.listen(443);

const http = require('http');

const redirectServer = http.createServer((req, res) => {
  const host = req.headers.host;
  res.writeHead(301, {
    Location: `https://${host}${req.url}`
  });
  res.end();
});

redirectServer.listen(80);

TLS 配置优化 #

javascript
const https = require('https');
const fs = require('fs');
const WebSocket = require('ws');

const tlsOptions = {
  cert: fs.readFileSync('/path/to/cert.pem'),
  key: fs.readFileSync('/path/to/key.pem'),
  ca: fs.readFileSync('/path/to/ca.pem'),
  
  minVersion: 'TLSv1.2',
  ciphers: [
    'ECDHE-ECDSA-AES128-GCM-SHA256',
    'ECDHE-RSA-AES128-GCM-SHA256',
    'ECDHE-ECDSA-AES256-GCM-SHA384',
    'ECDHE-RSA-AES256-GCM-SHA384'
  ].join(':'),
  
  honorCipherOrder: true,
  
  sessionTimeout: 3600,
  
  rejectUnauthorized: true
};

const server = https.createServer(tlsOptions);

const wss = new WebSocket.Server({
  server,
  perMessageDeflate: {
    zlibDeflateOptions: {
      level: 3
    },
    zlibInflateOptions: {
      chunkSize: 10 * 1024
    },
    clientNoContextTakeover: true,
    serverNoContextTakeover: true,
    serverMaxWindowBits: 10,
    clientMaxWindowBits: true
  }
});

server.listen(443);

安全最佳实践清单 #

text
┌─────────────────────────────────────────────────────────────┐
│                    WebSocket 安全检查清单                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  认证与授权                                                  │
│  ─────────────────────────────────────────────────────────  │
│  □ 实现身份认证机制                                         │
│  □ 使用安全的令牌传递方式                                   │
│  □ 实现基于角色的访问控制                                   │
│  □ 验证用户对资源的访问权限                                 │
│                                                             │
│  传输安全                                                    │
│  ─────────────────────────────────────────────────────────  │
│  □ 使用 WSS(加密传输)                                     │
│  □ 配置强 TLS 版本和密码套件                                │
│  □ 强制 HTTPS 重定向                                        │
│  □ 启用证书验证                                             │
│                                                             │
│  跨域安全                                                    │
│  ─────────────────────────────────────────────────────────  │
│  □ 验证 Origin 头部                                         │
│  □ 配置允许的来源白名单                                     │
│  □ 防止跨站 WebSocket 劫持                                  │
│                                                             │
│  输入验证                                                    │
│  ─────────────────────────────────────────────────────────  │
│  □ 验证消息格式和类型                                       │
│  □ 限制消息大小                                             │
│  □ 过滤和转义用户输入                                       │
│  □ 防止 XSS 和注入攻击                                      │
│                                                             │
│  速率限制                                                    │
│  ─────────────────────────────────────────────────────────  │
│  □ 实现连接速率限制                                         │
│  □ 实现消息速率限制                                         │
│  □ 限制单个 IP 的连接数                                     │
│  □ 设置合理的超时时间                                       │
│                                                             │
│  监控与日志                                                  │
│  ─────────────────────────────────────────────────────────  │
│  □ 记录连接和断开事件                                       │
│  □ 记录异常和错误                                           │
│  □ 监控连接数和资源使用                                     │
│  □ 设置安全告警                                             │
│                                                             │
│  错误处理                                                    │
│  ─────────────────────────────────────────────────────────  │
│  □ 不暴露敏感错误信息                                       │
│  □ 优雅处理异常                                             │
│  □ 实现断线重连机制                                         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

总结 #

WebSocket 安全是一个多层次的话题,需要从认证授权、传输加密、输入验证、速率限制等多个方面综合考虑。通过实施本指南中的安全措施,可以有效地保护 WebSocket 应用免受常见的安全威胁。

记住,安全是一个持续的过程,需要定期审查和更新安全策略,关注新的安全漏洞和最佳实践。

最后更新:2026-03-29