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