WebSocket 安全实践:从认证到加密

在前三篇文章中,我们深入探讨了 WebSocket 的基础原理、服务端开发和客户端实现。今天,让我们把重点放在安全性上,看看如何构建一个安全可靠的 WebSocket 应用。我曾在一个金融项目中,通过实施多层安全机制,成功防御了多次恶意攻击尝试。

安全挑战

WebSocket 应用面临的主要安全挑战包括:

  1. 身份认证
  2. 数据加密
  3. 跨站点 WebSocket 劫持(CSWSH)
  4. 拒绝服务攻击(DoS)
  5. 中间人攻击

让我们逐一解决这些问题。

身份认证

实现安全的身份认证机制:

javascript 复制代码
// auth-handler.js
class AuthHandler {
  constructor(options = {}) {
    this.options = {
      tokenSecret: process.env.TOKEN_SECRET,
      tokenExpiration: '24h',
      refreshTokenExpiration: '7d',
      ...options
    }

    this.blacklist = new Set()
  }

  // 生成访问令牌
  generateAccessToken(user) {
    return jwt.sign(
      {
        id: user.id,
        role: user.role,
        type: 'access'
      },
      this.options.tokenSecret,
      {
        expiresIn: this.options.tokenExpiration
      }
    )
  }

  // 生成刷新令牌
  generateRefreshToken(user) {
    return jwt.sign(
      {
        id: user.id,
        type: 'refresh'
      },
      this.options.tokenSecret,
      {
        expiresIn: this.options.refreshTokenExpiration
      }
    )
  }

  // 验证令牌
  verifyToken(token) {
    try {
      // 检查黑名单
      if (this.blacklist.has(token)) {
        throw new Error('Token has been revoked')
      }

      const decoded = jwt.verify(token, this.options.tokenSecret)

      // 验证令牌类型
      if (decoded.type !== 'access') {
        throw new Error('Invalid token type')
      }

      return decoded
    } catch (error) {
      throw new Error('Invalid token')
    }
  }

  // 刷新令牌
  async refreshToken(refreshToken) {
    try {
      const decoded = jwt.verify(refreshToken, this.options.tokenSecret)

      // 验证令牌类型
      if (decoded.type !== 'refresh') {
        throw new Error('Invalid token type')
      }

      // 获取用户信息
      const user = await this.getUserById(decoded.id)
      if (!user) {
        throw new Error('User not found')
      }

      // 生成新的访问令牌
      return this.generateAccessToken(user)
    } catch (error) {
      throw new Error('Invalid refresh token')
    }
  }

  // 吊销令牌
  revokeToken(token) {
    this.blacklist.add(token)
  }

  // 清理过期的黑名单令牌
  cleanupBlacklist() {
    this.blacklist.forEach(token => {
      try {
        jwt.verify(token, this.options.tokenSecret)
      } catch (error) {
        // 令牌已过期,从黑名单中移除
        this.blacklist.delete(token)
      }
    })
  }

  // WebSocket 握手认证
  handleHandshake(request) {
    return new Promise((resolve, reject) => {
      const token = this.extractToken(request)

      if (!token) {
        reject(new Error('No token provided'))
        return
      }

      try {
        const decoded = this.verifyToken(token)
        resolve(decoded)
      } catch (error) {
        reject(error)
      }
    })
  }

  // 从请求中提取令牌
  extractToken(request) {
    const auth = request.headers['authorization']
    if (!auth) return null

    const [type, token] = auth.split(' ')
    return type === 'Bearer' ? token : null
  }
}

数据加密

实现端到端加密:

javascript 复制代码
// encryption-handler.js
class EncryptionHandler {
  constructor(options = {}) {
    this.options = {
      algorithm: 'aes-256-gcm',
      keyLength: 32,
      ivLength: 16,
      tagLength: 16,
      ...options
    }
  }

  // 生成密钥对
  generateKeyPair() {
    return crypto.generateKeyPairSync('rsa', {
      modulusLength: 2048,
      publicKeyEncoding: {
        type: 'spki',
        format: 'pem'
      },
      privateKeyEncoding: {
        type: 'pkcs8',
        format: 'pem'
      }
    })
  }

  // 生成对称密钥
  generateSymmetricKey() {
    return crypto.randomBytes(this.options.keyLength)
  }

  // 加密消息
  encrypt(message, key) {
    // 生成初始化向量
    const iv = crypto.randomBytes(this.options.ivLength)

    // 创建加密器
    const cipher = crypto.createCipheriv(
      this.options.algorithm,
      key,
      iv,
      {
        authTagLength: this.options.tagLength
      }
    )

    // 加密数据
    let encrypted = cipher.update(message, 'utf8', 'base64')
    encrypted += cipher.final('base64')

    // 获取认证标签
    const tag = cipher.getAuthTag()

    return {
      encrypted,
      iv: iv.toString('base64'),
      tag: tag.toString('base64')
    }
  }

  // 解密消息
  decrypt(data, key) {
    const { encrypted, iv, tag } = data

    // 创建解密器
    const decipher = crypto.createDecipheriv(
      this.options.algorithm,
      key,
      Buffer.from(iv, 'base64'),
      {
        authTagLength: this.options.tagLength
      }
    )

    // 设置认证标签
    decipher.setAuthTag(Buffer.from(tag, 'base64'))

    // 解密数据
    let decrypted = decipher.update(encrypted, 'base64', 'utf8')
    decrypted += decipher.final('utf8')

    return decrypted
  }

  // 使用公钥加密
  encryptWithPublicKey(data, publicKey) {
    return crypto.publicEncrypt(
      {
        key: publicKey,
        padding: crypto.constants.RSA_PKCS1_OAEP_PADDING
      },
      Buffer.from(data)
    ).toString('base64')
  }

  // 使用私钥解密
  decryptWithPrivateKey(data, privateKey) {
    return crypto.privateDecrypt(
      {
        key: privateKey,
        padding: crypto.constants.RSA_PKCS1_OAEP_PADDING
      },
      Buffer.from(data, 'base64')
    ).toString()
  }
}

安全的 WebSocket 服务器

整合认证和加密的安全服务器:

javascript 复制代码
// secure-websocket-server.js
class SecureWebSocketServer {
  constructor(options = {}) {
    this.options = {
      port: 8080,
      path: '/ws',
      ...options
    }

    this.authHandler = new AuthHandler()
    this.encryptionHandler = new EncryptionHandler()
    this.clients = new Map()

    this.initialize()
  }

  // 初始化服务器
  initialize() {
    // 创建 HTTPS 服务��
    this.server = https.createServer({
      key: fs.readFileSync('server.key'),
      cert: fs.readFileSync('server.cert')
    })

    // 创建 WebSocket 服务器
    this.wss = new WebSocket.Server({
      server: this.server,
      path: this.options.path,
      verifyClient: this.verifyClient.bind(this)
    })

    // 设置事件处理器
    this.setupEventHandlers()

    // 启动服务器
    this.server.listen(this.options.port)
  }

  // 验证客户端连接
  async verifyClient(info, callback) {
    try {
      // 验证令牌
      const user = await this.authHandler.handleHandshake(info.req)

      // 生成会话密钥
      const sessionKey = this.encryptionHandler.generateSymmetricKey()

      // 存储客户端信息
      info.req.client = {
        user,
        sessionKey
      }

      callback(true)
    } catch (error) {
      console.error('Authentication failed:', error)
      callback(false, 401, 'Unauthorized')
    }
  }

  // 设置事件处理器
  setupEventHandlers() {
    this.wss.on('connection', (ws, req) => {
      const { user, sessionKey } = req.client

      // 存储客户端信息
      this.clients.set(ws, {
        user,
        sessionKey,
        lastActivity: Date.now()
      })

      // 发送会话密钥
      this.sendSessionKey(ws, user, sessionKey)

      // 设置消息处理器
      ws.on('message', (message) => {
        this.handleMessage(ws, message)
      })

      // 设置关闭处理器
      ws.on('close', () => {
        this.handleClose(ws)
      })

      // 设置错误处理器
      ws.on('error', (error) => {
        this.handleError(ws, error)
      })
    })
  }

  // 发送会话密钥
  sendSessionKey(ws, user, sessionKey) {
    // 使用客户端公钥加密会话密钥
    const encryptedKey = this.encryptionHandler.encryptWithPublicKey(
      sessionKey,
      user.publicKey
    )

    ws.send(JSON.stringify({
      type: 'session_key',
      key: encryptedKey
    }))
  }

  // 处理消息
  handleMessage(ws, message) {
    const client = this.clients.get(ws)
    if (!client) return

    try {
      // 解密消息
      const decrypted = this.encryptionHandler.decrypt(
        JSON.parse(message),
        client.sessionKey
      )

      // 处理解密后的消息
      const data = JSON.parse(decrypted)
      this.processMessage(ws, client, data)

      // 更新最后活动时间
      client.lastActivity = Date.now()
    } catch (error) {
      console.error('Message handling error:', error)
      this.handleError(ws, error)
    }
  }

  // 处理消息内容
  processMessage(ws, client, message) {
    // 验证消息权限
    if (!this.canProcessMessage(client.user, message)) {
      this.sendError(ws, 'Permission denied')
      return
    }

    // 处理不同类型的消息
    switch (message.type) {
      case 'chat':
        this.handleChatMessage(ws, client, message)
        break
      case 'action':
        this.handleActionMessage(ws, client, message)
        break
      default:
        this.sendError(ws, 'Unknown message type')
    }
  }

  // 发送加密消息
  sendEncrypted(ws, data) {
    const client = this.clients.get(ws)
    if (!client) return

    try {
      // 加密消息
      const encrypted = this.encryptionHandler.encrypt(
        JSON.stringify(data),
        client.sessionKey
      )

      ws.send(JSON.stringify(encrypted))
    } catch (error) {
      console.error('Send error:', error)
      this.handleError(ws, error)
    }
  }

  // 处理连接关闭
  handleClose(ws) {
    const client = this.clients.get(ws)
    if (!client) return

    // 清理客户端信息
    this.clients.delete(ws)
  }

  // 处理错误
  handleError(ws, error) {
    console.error('WebSocket error:', error)

    // 发送错误消息
    this.sendError(ws, 'Internal server error')

    // 关闭连接
    ws.close()
  }

  // 发送错误消息
  sendError(ws, message) {
    this.sendEncrypted(ws, {
      type: 'error',
      message
    })
  }

  // 验证消息权限
  canProcessMessage(user, message) {
    // 实现权限验证逻辑
    return true
  }

  // 清理不活跃的连接
  cleanup() {
    const now = Date.now()
    const timeout = 5 * 60 * 1000 // 5 分钟超时

    this.clients.forEach((client, ws) => {
      if (now - client.lastActivity > timeout) {
        console.log(`Cleaning up inactive client: ${client.user.id}`)
        ws.close()
        this.clients.delete(ws)
      }
    })
  }

  // 优雅关闭
  shutdown() {
    console.log('Shutting down secure WebSocket server...')

    // 关闭所有连接
    this.wss.clients.forEach(client => {
      client.close()
    })

    // 关闭服务器
    this.server.close(() => {
      console.log('Server closed')
    })
  }
}

安全的 WebSocket 客户端

实现安全的客户端:

javascript 复制代码
// secure-websocket-client.js
class SecureWebSocketClient {
  constructor(url, options = {}) {
    this.url = url
    this.options = {
      reconnectInterval: 1000,
      maxReconnectAttempts: 5,
      ...options
    }

    this.authHandler = new AuthHandler()
    this.encryptionHandler = new EncryptionHandler()
    this.sessionKey = null
    this.keyPair = null

    this.initialize()
  }

  // 初始化客户端
  async initialize() {
    // 生成密钥对
    this.keyPair = this.encryptionHandler.generateKeyPair()

    // 连接服务器
    await this.connect()
  }

  // 建立连接
  async connect() {
    try {
      // 获取访问令牌
      const token = await this.authHandler.getAccessToken()

      // 创建 WebSocket 连接
      this.ws = new WebSocket(this.url, {
        headers: {
          'Authorization': `Bearer ${token}`
        }
      })

      // 设置事件处理器
      this.setupEventHandlers()
    } catch (error) {
      console.error('Connection error:', error)
      this.handleReconnect()
    }
  }

  // 设置事件处理器
  setupEventHandlers() {
    this.ws.onopen = () => {
      console.log('Connected to secure WebSocket server')
    }

    this.ws.onmessage = (event) => {
      this.handleMessage(event.data)
    }

    this.ws.onclose = () => {
      console.log('Disconnected from secure WebSocket server')
      this.handleReconnect()
    }

    this.ws.onerror = (error) => {
      console.error('WebSocket error:', error)
    }
  }

  // 处理消息
  handleMessage(data) {
    try {
      const message = JSON.parse(data)

      // 处理会话密钥
      if (message.type === 'session_key') {
        this.handleSessionKey(message.key)
        return
      }

      // 解密消息
      const decrypted = this.encryptionHandler.decrypt(
        message,
        this.sessionKey
      )

      // 处理解密后的消息
      this.processMessage(JSON.parse(decrypted))
    } catch (error) {
      console.error('Message handling error:', error)
    }
  }

  // 处理会话密钥
  handleSessionKey(encryptedKey) {
    try {
      // 使用私钥解密会话密钥
      const key = this.encryptionHandler.decryptWithPrivateKey(
        encryptedKey,
        this.keyPair.privateKey
      )

      this.sessionKey = Buffer.from(key)
      console.log('Session key established')
    } catch (error) {
      console.error('Session key handling error:', error)
      this.ws.close()
    }
  }

  // 发送加密消息
  send(data) {
    if (!this.sessionKey) {
      console.error('No session key available')
      return
    }

    try {
      // 加密消息
      const encrypted = this.encryptionHandler.encrypt(
        JSON.stringify(data),
        this.sessionKey
      )

      this.ws.send(JSON.stringify(encrypted))
    } catch (error) {
      console.error('Send error:', error)
    }
  }

  // 处理重连
  handleReconnect() {
    if (this.reconnectAttempts >= this.options.maxReconnectAttempts) {
      console.log('Max reconnection attempts reached')
      return
    }

    this.reconnectAttempts++

    setTimeout(() => {
      this.connect()
    }, this.options.reconnectInterval)
  }

  // 关闭连接
  close() {
    if (this.ws) {
      this.ws.close()
    }
  }
}

最佳实践

  1. 使用 HTTPS

    • 始终在 WSS (WebSocket Secure) 上运行 WebSocket
    • 配置强加密套件
    • 定期更新 SSL 证书
  2. 身份认证

    • 实现基于令牌的认证
    • 使用短期访问令牌和长期刷新令牌
    • 实现令牌轮换机制
  3. 数据加密

    • 使用端到端加密保护消息
    • 实现安全的密钥交换
    • 定期轮换会话密钥
  4. 输入验证

    • 验证所有客户端输入
    • 实现消息大小限制
    • 防止注入攻击
  5. 速率限制

    • 实现连接限制
    • 实现消息速率限制
    • 防止 DoS 攻击
  6. 错误处理

    • 实现优雅的错误处理
    • 不泄露敏感信息
    • 记录安全事件
  7. 监控和日志

    • 实现安全日志
    • 监控异常行为
    • 设置警报机制

写在最后

通过这篇文章,我们深入探讨了如何构建安全的 WebSocket 应用。从身份认证到数据加密,从安全最佳实践到具体实现,我们不仅关注了功能实现,更注重了实际应用中的安全挑战。

记住,安全是一个持续的过程,需要不断更新和改进。在实际开发中,我们要始终将安全放在首位,确保应用能够安全可靠地运行。

如果觉得这篇文章对你有帮助,别忘了点个赞 👍

相关推荐
HUIBUR科技31 分钟前
量子计算遇上人工智能:突破算力瓶颈的关键?
人工智能·量子计算
CES_Asia32 分钟前
CES Asia 2025聚焦量子与空间技术
人工智能·科技·数码相机·金融·量子计算·智能手表
程序猿阿伟36 分钟前
《量子比特:解锁人工智能并行计算加速的密钥》
人工智能·量子计算
盖丽男2 小时前
机器学习的组成
人工智能·机器学习
风一样的树懒3 小时前
Python使用pip安装Caused by SSLError:certificate verify failed
人工智能·python
9命怪猫3 小时前
AI大模型-提示工程学习笔记5-零提示
人工智能·笔记·学习·ai·提示工程
顾尘眠3 小时前
http常用状态码(204,304, 404, 504,502)含义
前端
cnbestec3 小时前
GelSight Mini视触觉传感器凝胶触头升级:增加40%耐用性,拓展机器人与触觉AI 应用边界
人工智能·机器人
bohu833 小时前
ros2-4.2 用python实现人脸识别
人工智能·opencv·人脸识别·ros2·服务调用
Loving_enjoy4 小时前
ChatGPT 数据分析与处理使用详解
大数据·人工智能