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

news2025/1/9 22:57:37

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

安全挑战

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

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

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

身份认证

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

// 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
  }
}

数据加密

实现端到端加密:

// 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 服务器

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

// 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 客户端

实现安全的客户端:

// 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 应用。从身份认证到数据加密,从安全最佳实践到具体实现,我们不仅关注了功能实现,更注重了实际应用中的安全挑战。

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2274038.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Shell脚本】Docker构建Java项目,并自动停止原镜像容器,发布新版本

本文简述 经常使用docker部署SpringBoot 项目,因为自己的服务器小且项目简单,因此没有使用自动化部署。每次将jar包传到服务器后,需要手动构建,然后停止原有容器,并使用新的镜像启动,介于AI时代越来越懒的…

关于ssh-server在windows系统中进行部署及通过mobaxterm中ssh隧道技术实现不同网段之间进行网络通讯的问题

问题1.windows系统部署ssh-server 在安装部署过程中参考先行者就可实现部署。我使用的作者百度云安装包。 记录一下操作步骤: 1.在搜索中打开power shell命令行,将文件夹复制到C:\Program Files,切换到OpenSSH-Win64,执行如下安装…

写了个小工具,绿色/C#/Url/Base64/Encode/Decode

写这个小工具的动机是什么呢? 虽然很多在线工具也非常地方便,但经常在抓包的时候需要操作相关的转码工作,但你开着抓包工具访问网页有时候又非常地不方便。这时候就想到如果有一款本地的工具软件,就非常地耐斯。 这种工具也不是…

【优选算法】Binary-Blade:二分查找的算法刃(下)

文章目录 1.山脉数组的峰顶索引2.寻找峰值3.寻找旋转排序数组中的最小值4.点名希望读者们多多三连支持小编会继续更新你们的鼓励就是我前进的动力! 本篇接上一篇二分查找,主要通过部分题目熟悉二分查找的进阶使用,重点强调二段性,…

【Ubuntu22.04】VMware虚拟机硬盘扩容

1.首先打开虚拟机设置 2.根据需要对硬盘扩展 这边提示我们还需要进入虚拟机在内部分区 3.安装界面化磁盘管理工具 # 安装 sudo apt install gparted# 启动 sudo gparted调整硬盘大小 调整的时候会提示我们硬盘是只读的,因此还要进行操作 新建终端重新挂载文件系…

无网络时自动切换备用网络环境

目录 背景目标为什么需要做自动网络切换网络切换手段 网络环境实现思路和代码部署脚本开机自动执行附录连接两个网络时的路由问题 背景 目标 学校实验室有两个网络环境,我电脑使用网线连接稳定但低速的网络A,使用WiFi连接高速但不稳定的网络B。因此&am…

设计模式 行为型 策略模式(Strategy Pattern)与 常见技术框架应用 解析

策略模式(Strategy Pattern)核心思想是将算法的实现从使用该算法的类中分离出来,作为独立的对象,通过接口来定义算法家族,这样就可以很容易地改变或扩展算法。通过这种方式,可以避免在客户端代码中使用大量…

Unity 热更新基础知识

文章目录 1.一些名词2.三种编译方式3.Unity 两种脚本后端3.1 Mono3.2 IL2CPP3.3 对比 1.一些名词 IL(Intermediate Language):中间语言(类似于汇编代码)CIL(Common Intermediate Language)&…

C++感受15-Hello STL 泛型启蒙

生鱼片和STL的关系,你听过吗?泛型编程和面向对象编程,它们打架吗?行为泛型和数据泛型,各自的目的是? 0 楔 俄罗斯生鱼片,号称俄罗斯版的中国烤鸭,闻名于世。其鱼肉,源于…

LabVIEW轴承性能测试系统

本文介绍了基于LabVIEW的高效轴承性能测试系统的设计与开发。系统通过双端驱动技术实现高精度同步控制,针对轴承性能进行全面的测试与分析,以提高轴承的可靠性和寿命。 项目背景 随着工业自动化程度的提高,对轴承的性能要求越来越高。传统的…

(k8s)Flannel Error问题解决!

1.问题描述 书接上回,我们在解决kubectl不断重启的时候引入了Flannel 网络插件,但是一上来就报错, 2.问题解决 自己的思路:照例开始检查 1.先检查一下目前Flannel的pod kubectl get pods --all-namespaces 2.检查 Flannel的po…

CatLog的使用

一 CatLog的简介 1.1 作用 CAT(Central Application Tracking) 是基于 Java 开发的实时应用监控平台,为美团点评提供了全面的实时监控告警服务。 1.2 组成部分 1.2.1 Transaction 1.Transaction 适合记录跨越系统边界的程序访问行为&a…

深入Android架构(从线程到AIDL)_18 SurfaceView的UI多线程02

目录 2、 使用SurfaceView画2D图 范例一 设计GameLoop(把小线程移出来) 范例二 2、 使用SurfaceView画2D图 范例一 以SurfaceView绘出Bitmap图像设计SpriteView类别来实作SurfaceHolder.Callback接口首先来看个简单的程序,显示出一个Bitmap图像。这个图像就构…

【FlutterDart】 拖动边界线改变列宽类似 vscode 那种拖动改变编辑框窗口大小(11 /100)

【Flutter&Dart】 拖动改变 widget 的窗口尺寸大小GestureDetector~简单实现(10 /100) 【Flutter&Dart】 拖动边界线改变列宽并且有边界高亮和鼠标效果(12 /100) 上效果: 这个在知乎里找到的效果&…

tk GMV MAX素材范围投放指南

Product GMy Max素材范围说明 Product GMy Max能自动获取带有相关商品锚点链接(无论是单个锚点还是多个锚点)的视频,并将其用于推广特定商品的广告素材,前提是这些视频已经获得广告授权。然而,请注意,多个…

物联网无线芯片模组方案,设备智能化交互升级,ESP32-C3控制应用

无线交互技术的核心在于实现设备之间的无缝连接和数据传输。在智能家居系统中,各种智能设备如智能灯泡、智能插座、智能门锁等,都通过无线网络相互连接,形成一个互联互通的生态。 用户可以通过语音助手、手机APP或其他智能终端,远…

ubuntu为Docker配置代理

终端代理 我们平常在ubuntu终端中使用curl或git命令时,往往会很慢。 所以,首先需要给ubuntu终端环境添加代理。 查看自身那个软件的端口号,我这里是7890。 sudo gedit ~/.bashrcexport http_proxyhttp://localhost:7890 export https_pr…

【VUE 指令学习笔记】

v-bind :单向绑定解析表达式,可简写为:xxx v-model :双向数据绑定。 v-for:遍历数组/对象/字符串 v-on:绑定事件监听,可简写为。 v-if:条件渲染(动态控制节点是否存存在) v-else:条件渲染(动态控制节点是否存存在) v-show:条件渲染…

用OpenCV实现UVC视频分屏

分屏 OpencvUVC代码验证后话 用OpenCV实现UVC摄像头的视频分屏。 Opencv opencv里有很多视频图像的处理功能。 UVC Usb 视频类,免驱动的。视频流格式有MJPG和YUY2。MJPG是RGB三色通道的。要对三通道进行分屏显示。 代码 import cv2 import numpy as np video …

123.【C语言】数据结构之快速排序挖坑法和前后指针法

目录 1.挖坑法 执行流程 代码 运行结果 可读性好的代码 2.前后指针法(双指针法) 执行流程 单趟排序代码 将单趟排序代码改造后 写法1 简洁的写法 3.思考题 1.挖坑法 执行流程 "挖坑法"顾名思义:要有坑位,一开始将关键值放入临时变量key中,在数组中形成…