在现代 Web 应用开发中,WebSocket 作为全双工通信协议,被广泛用于实时聊天、消息推送和数据同步等场景。然而,面对需要用户身份认证的接口,很多开发者都会遇到一个共同困惑:WebSocket 握手阶段能否像 HTTP 请求那样携带 Token? 本文将从实践角度出发,深度剖析几种主流的 Token 传递方案,并给出安全、高效的推荐实现。

为什么 WebSocket 认证是个难题?

WebSocket 协议在设计之初,其握手过程虽然基于 HTTP,但一旦升级为 WebSocket 连接,浏览器原生 API 便不再允许开发者自定义 HTTP Header。这意味着,在 JavaScript 或 TypeScript 环境下,我们无法直接通过 setRequestHeader 来附加 Authorization 字段。对于使用 Go、Java 等后端语言实现的服务端,虽然可以解析握手请求头,但前端浏览器端的限制却成了天然的壁垒。

因此,开发者必须另辟蹊径,通过 URL 参数、子协议(Subprotocol)或连接建立后的首条消息来传递身份凭证。接下来,我们将逐一解析这些方案的优缺点。

✅ 方案一:URL 查询参数传递(简单直接)

最直观的思路是将 Token 拼接在 WebSocket 的 URL 后面,作为查询参数传递。这种方式实现成本极低,无论是 JavaScript、Java 还是 Go 的 WebSocket 客户端库,都支持直接拼接字符串。

其核心代码结构如下,将 Token 附加在 ws://wss:// 地址之后:

// 在创建 WebSocket 连接时,将 token 添加到 URL 中
const token = 'your-token-here'; // 从你的 auth 模块获取 token
ws.value = new WebSocket(`${wsUrl.value}?token=${encodeURIComponent(token)}`);

为了更清晰地展示,下面是一个完整的连接示例,包含了从获取 Token 到建立连接的完整逻辑:

const initWebSocket = () => {
  if (ws.value && ws.value.readyState === WebSocket.OPEN) {
    ws.value.close();
  }
  // 获取 token(从你的 auth 模块获取)
  const token = getToken(); // 假设你有 getToken() 函数
  try {
    // 将 token 添加到 URL 中
    const fullWsUrl = `${wsUrl.value}?token=${encodeURIComponent(token)}`;
    ws.value = new WebSocket(fullWsUrl);
    console.log('ws', ws.value);
    ws.value.onopen = () => {
      console.log('WebSocket 连接成功');
    };
    // ... 其他事件处理代码保持不变
  } catch (err) {
    console.log('initWebSocket error', err);
  }
};

⚠️ 注意事项: 在使用该方案时,务必使用 encodeURIComponent 对 Token 进行编码,防止特殊字符破坏 URL 结构。

encodeURIComponent()

这种方案虽然简单,但存在一个致命弱点:Token 会暴露在浏览器历史记录和服务器访问日志中,存在泄露风险。不过对于内部系统或短期连接,它依然是效率最高的选择。

️ 方案二:利用子协议(Subprotocol)传递 Header

如果坚持希望将 Token 放在 Header 中,可以借助 WebSocket 的 Sec-WebSocket-Protocol 扩展能力。但这需要服务端与客户端的紧密配合,且并非所有后端框架(如 Go 的 Gorilla、Java 的 Netty)都默认支持自定义解析。

实现思路通常是:在客户端连接时,将 Token 作为子协议名称传入,服务端在握手阶段解析该字段并校验。但这种方式需要引入额外的库或自定义协议处理逻辑,复杂度较高。

// 需要先安装:npm install reconnecting-websocket
import ReconnectingWebSocket from 'reconnecting-websocket';
const token = getToken();
const rws = new ReconnectingWebSocket(wsUrl.value, [], {
  connectionTimeout: 10000,
  maxRetries: 3,
  headers: {
    'Authorization': `Bearer ${token}`,
    // 或者
    'X-Auth-Token': token
  }
});

实际上,大多数开发者并不会采用上述复杂方式,而是选择更优雅的第三种方案。

方案三:连接建立后立即发送认证消息(安全首选)

这是目前公认的最佳实践。在 WebSocket 连接成功建立的瞬间,客户端立即发送一条包含 Token 的 JSON 消息。服务端收到后校验 Token 合法性,决定是接受该连接还是主动断开。

这种方案的优势在于:Token 不经过 URL,不会产生日志泄露。同时,它完美规避了浏览器无法自定义 Header 的限制,适用于所有编程语言(C++、JavaScript、Go 等)的客户端。

ws.value.onopen = () => {
  console.log('WebSocket 连接成功');
  // 连接建立后立即发送认证消息
  const token = getToken();
  const authMessage = {
    type: 'auth',
    token: token
  };
  ws.value.send(JSON.stringify(authMessage));
};

在服务端(以 Java 或 Go 为例),需要监听首条消息并校验。如果校验失败,服务端应主动调用 Close() 方法关闭连接,避免无效连接占用资源。

推荐实现与代码整合

综合考量安全性、跨语言兼容性和实现成本,强烈推荐使用“URL 参数”或“认证消息”方案。如果你使用的是 JavaScript/TypeScript 技术栈,且拥有现成的 getToken() 鉴权模块,可以这样整合:

const initWebSocket = () => {
  if (ws.value && ws.value.readyState === WebSocket.OPEN) {
    ws.value.close();
  }
  try {
    // 从你的 auth 模块获取 token
    const token = getToken();
    // 将 token 添加到 WebSocket URL 中
    const fullWsUrl = token
      ? `${wsUrl.value}?token=${encodeURIComponent(token)}`
      : wsUrl.value;
    ws.value = new WebSocket(fullWsUrl);
    console.log('ws', ws.value);
    ws.value.onopen = () => {
      console.log('WebSocket 连接成功');
    };
    ws.value.onmessage = (e) => {
      // ... 你现有的消息处理代码
    };
    ws.value.onclose = () => {
      console.log('连接已关闭');
    };
  } catch (err) {
    console.log('initWebSocket error', err);
  }
};

对于追求极致安全的场景,建议使用认证消息方案。以下是结合 getToken() 函数的完整推荐实现:

const initWebSocket = () => {
  if (ws.value && ws.value.readyState === WebSocket.OPEN) {
    ws.value.close();
  }
  const token = `Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImZjZTFjZWQ5LTM0ODYtNGQ5Yy05Y2Q4LTI2YzliMzI0MWFjYyJ9.PVvyxvNINiucOZuFadHMaL-7K7hwzeQX9aVpAciNRLMyLGcF7ajKZ3nhKUM6v0rXY8E552_UnCOjp6EA5HrsWw`;
  try {
    // 不在 URL 中传递 token
    ws.value = new WebSocket(wsUrl.value);
    console.log('ws', ws.value);
    ws.value.onopen = () => {
      console.log('WebSocket 连接成功');
      // 连接建立后立即发送认证消息
      const authMessage = {
        type: 'auth',
        token: token
      };
      ws.value.send(JSON.stringify(authMessage));
    };
    ws.value.onmessage = (e) => {
      // 检查是否是认证响应
      if (e.data && typeof e.data === 'string') {
        try {
          const res = JSON.parse(e.data);
          if (res.type === 'auth_response') {
            if (res.success) {
              console.log('WebSocket 认证成功');
            } else {
              console.error('WebSocket 认证失败:', res.message);
              ws.value.close();
            }
            return;
          }
        } catch (error) {
          // 如果不是 JSON 格式的消息,继续处理其他消息
        }
      }
      // 你现有的消息处理代码
      if (e.data === 'DONE') {
        console.log('结束信号');
        ws.value.close();
        return;
      }
      // ... 其他消息处理逻辑保持不变
      const res = JSON.parse(e.data);
      // ... 你的现有处理逻辑
    };
    ws.value.onclose = () => {
      console.log('连接已关闭');
    };
    ws.value.onerror = (error) => {
      console.error('WebSocket 错误:', error);
    };
  } catch (err) {
    console.log('initWebSocket error', err);
  }
};

此外,如果你使用的后端框架支持自定义握手拦截器(如 Spring 的 HandshakeInterceptor),也可以尝试通过第三方库增强功能,但需谨慎评估维护成本。

npm install reconnecting-websocket
import ReconnectingWebSocket from 'reconnecting-websocket';
const initWebSocket = () => {
  if (ws.value) {
    ws.value.close();
  }
  const token = `Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImZjZTFjZWQ5LTM0ODYtNGQ5Yy05Y2Q4LTI2YzliMzI0MWFjYyJ9.PVvyxvNINiucOZuFadHMaL-7K7hwzeQX9aVpAciNRLMyLGcF7ajKZ3nhKUM6v0rXY8E552_UnCOjp6EA5HrsWw`;
  const rws = new ReconnectingWebSocket(wsUrl.value, [], {
    connectionTimeout: 10000,
    maxRetries: 3,
    WebSocket: class extends WebSocket {
      constructor(url) {
        super(url);
        // 设置自定义头部(需要服务器支持)
        this.protocol = token; // 这个方式可能不被所有服务器支持
      }
    }
  });
  ws.value = rws;
  // ... 其他事件处理保持不变
};

️ 进阶:基于现有 Auth 模块的无缝集成

在实际项目中,Token 通常由统一的 Auth 模块管理。我们可以将该模块与 WebSocket 连接逻辑解耦,实现优雅集成:

import { getToken } from '@/package/utils/auth';
const initWebSocket = () => {
  if (ws.value && ws.value.readyState === WebSocket.OPEN) {
    ws.value.close();
  }
  // 从 auth 模块获取 token
  const token = getToken();
  if (!token) {
    console.error('未获取到 token,无法建立 WebSocket 连接');
    return;
  }
  const fullToken = `Bearer ${token}`;
  try {
    ws.value = new WebSocket(wsUrl.value);
    console.log('ws', ws.value);
    ws.value.onopen = () => {
      console.log('WebSocket 连接成功');
      // 发送认证消息
      const authMessage = {
        type: 'auth',
        token: fullToken
      };
      ws.value.send(JSON.stringify(authMessage));
    };
    // ... 其他处理逻辑保持不变
  } catch (err) {
    console.log('initWebSocket error', err);
  }
};

以下是一段融合了错误处理与连接关闭的完整代码示例,确保在认证失败时及时释放资源:

import { ref, reactive, watch, onUnmounted, onMounted, nextTick } from 'vue';
import { CircleCloseFilled, Top } from '@element-plus/icons-vue';
import { BubbleList, Sender, Typewriter } from 'vue-element-plus-x';
import { getToken } from '@/package/utils/auth'; // 导入你的 auth 模块
// ... 其他代码保持不变
const initWebSocket = () => {
  if (ws.value && ws.value.readyState === WebSocket.OPEN) {
    ws.value.close();
  }
  // 从 auth 模块获取 token
  const token = getToken();
  if (!token) {
    console.error('未获取到 token,无法建立 WebSocket 连接');
    return;
  }
  const fullToken = `Bearer ${token}`;
  try {
    ws.value = new WebSocket(wsUrl.value);
    console.log('ws', ws.value);
    ws.value.onopen = () => {
      console.log('WebSocket 连接成功');
      // 发送认证消息
      const authMessage = {
        type: 'auth',
        token: fullToken
      };
      ws.value.send(JSON.stringify(authMessage));
    };
    ws.value.onmessage = (e) => {
      // 首先检查是否是认证响应
      if (e.data && typeof e.data === 'string') {
        try {
          const res = JSON.parse(e.data);
          // 如果是认证响应
          if (res.type === 'auth_response') {
            if (res.success) {
              console.log('WebSocket 认证成功');
            } else {
              console.error('WebSocket 认证失败:', res.message);
              ws.value.close();
            }
            return;
          }
        } catch (error) {
          // 如果不是 JSON 格式的消息,继续处理其他消息
        }
      }
      // 原有的消息处理逻辑
      if (e.data === 'DONE') {
        console.log('结束信号');
        ws.value.close();
        return;
      }
      console.log('接收data:', e.data);
      const res = JSON.parse(e.data);
      const msgIndex = res.index;
      if (msgIndex >= 0 && msgIndex < dataList.value.length) {
        const message = dataList.value[msgIndex];
        console.log('message', message);
        if (message) {
          switch (res.type) {
            case 'ai_response':
              message.aiAnswer += res.content;
              message.status = 'done';
              break;
            case 'ai_stream':
              message.aiAnswer += res.chunk;
              message.status = 'streaming';
              break;
            case 'error':
              message.aiAnswer = `[Error] ${res.msg}`;
              message.status = 'error';
              break;
          }
        }
      }
      nextTick(() => {
        scrollToBottom();
      });
      console.log('dataList.value', dataList.value);
    };
    ws.value.onclose = () => {
      console.log('连接已关闭');
    };
    ws.value.onerror = (error) => {
      console.error('WebSocket 错误:', error);
    };
  } catch (err) {
    console.log('initWebSocket error', err);
  }
};
// ... 其他代码保持不变

[AFFILIATE_SLOT_1]

最佳实践清单与安全建议

为了帮助你在不同技术栈(如 C++ 后端、Java Netty 服务端)中少走弯路,这里总结了几条核心准则:

  • 优先使用认证消息方案:避免 Token 出现在任何日志系统中,降低泄露风险。
  • URL 参数方案需谨慎:仅适用于内网或低安全等级场景,并务必对 Token 进行编码。
  • 服务端必须校验:无论前端采用何种方式,后端都需要在连接建立后的极短时间内完成 Token 验证。
  • 失败即断开:一旦认证失败,立即关闭连接,防止恶意请求消耗服务器资源。
  • 支持 Token 刷新:若 Token 过期,可设计重新认证的协议消息,避免频繁重连。

扩展:使用第三方库增强 Header 支持

如果你使用的是 Node.js 环境,或者通过某些特定客户端库(例如支持自定义 Header 的库),可以尝试以下方式。但需要安装额外的依赖包:

ReconnectingWebSocket
npm install reconnecting-websocket
import ReconnectingWebSocket from 'reconnecting-websocket';
const initWebSocket = () => {
  if (ws.value) {
    ws.value.close();
  }
  const token = `Bearer eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImZjZTFjZWQ5LTM0ODYtNGQ5Yy05Y2Q4LTI2YzliMzI0MWFjYyJ9.PVvyxvNINiucOZuFadHMaL-7K7hwzeQX9aVpAciNRLMyLGcF7ajKZ3nhKUM6v0rXY8E552_UnCOjp6EA5HrsWw`;
  const rws = new ReconnectingWebSocket(wsUrl.value, [], {
    connectionTimeout: 10000,
    maxRetries: 3,
    WebSocket: class extends WebSocket {
      constructor(url) {
        super(url);
        // 设置自定义头部(需要服务器支持)
        this.protocol = token; // 这个方式可能不被所有服务器支持
      }
    }
  });
  ws.value = rws;
  // ... 其他事件处理保持不变
};

需要注意的是,这种方式依赖服务端的特定实现,通用性较差,仅作为备选方案。

总结

WebSocket 携带 Token 的方式多种多样,但核心原则始终是安全与便捷的平衡。对于大多数应用场景,在连接建立后立即发送认证消息是兼顾安全性与跨平台兼容性的最佳选择。而 URL 参数方案则更适合对性能要求极高、安全要求较低的内部服务。希望本文的剖析能帮助你在 Go、Java 或 JavaScript 项目中做出最合适的架构决策。

[AFFILIATE_SLOT_2]