在 WebSocket 开发中,很多开发者会遇到一个常见误区:想像 HTTP 请求那样自定义任意请求头(比如 Authorization 鉴权),但实际上浏览器原生 WebSocket API 并不支持这一操作。本文将重点讲解 WebSocket 唯一官方允许的自定义"伪请求头"------Sec-WebSocket-Protocol 的具体实现,结合 Vue 前端与 SpringBoot 后端,提供可直接复用的核心代码。
一、核心前提:浏览器原生 WebSocket 的传参限制
浏览器原生 WebSocket API 不支持自定义任意请求头,不像 axios、fetch 那样可以通过 headers 配置项添加 Authorization、X-User-Id 等自定义头。
浏览器原生 WebSocket 仅支持两种官方传参方式:
-
URL 查询参数(如 ws://localhost:8080/ws?token=xxx),兼容性最好,但参数会暴露在 URL 中,安全性一般;【var ws = new WebSocket("ws://url?userid=1");】
-
Sec-WebSocket-Protocol 子协议,本质是 WebSocket 握手阶段的请求头字段,可用于传递鉴权信息,比 URL 参数更规范、更安全。
注意:我们平时看到的 WebSocket 自定义 headers 写法,大多是** socket.io、ws 等第三方库封装的**,并非原生 API 支持,纯原生开发需避开这一误区。
二、Sec-WebSocket-Protocol 核心原理
Sec-WebSocket-Protocol 是 WebSocket 官方定义的请求头字段,用于指定客户端与服务器之间的通信子协议。其核心作用的延伸的是:可将鉴权信息(如 Token)作为子协议值传入,后端在握手阶段获取该请求头,实现鉴权逻辑。
工作流程:
-
前端创建 WebSocket 实例时,传入第二个参数(子协议值);
-
浏览器自动将该参数写入请求头 Sec-WebSocket-Protocol;
-
后端在 WebSocket 握手阶段(OnOpen 方法)获取该请求头(Sec-WebSocket-Protocol),解析出鉴权信息;
-
后端完成鉴权后,正常建立 WebSocket 连接。
限制:Sec-WebSocket-Protocol 的值仅支持英文字母、数字、短横线,不能包含中文或特殊符号(如空格、@、# 等),因此传递 Token 时需注意格式。
三、Vue 前端核心实现(原生 WebSocket,无第三方库)
代码基于 Vue3(Setup 语法)实现,Vue2 写法类似,核心逻辑一致------通过 WebSocket 实例的第二个参数传递子协议(携带 Token)。
前端代码格式:
let socket = new WebSocket(url,[protocols]);
后台接收到websocket连接后,可以通过下述代码获取到token值:
request.getHeader("Sec-WebSocket-Protocol")
核心代码:
TypeScript
<script setup>
import { onMounted } from 'vue';
onMounted(() => {
// 1. 从本地存储(localStorage)获取鉴权 Token
const token = localStorage.getItem('token') || '';
// 2. WebSocket 连接地址
const wsUrl = 'ws://localhost:8080/ws';
// 3. 第二个参数传入子协议,格式自定义(此处用 Bearer- 前缀区分 Token)
// 支持字符串(单个值)或数组(多个值)
const ws = new WebSocket(wsUrl, `Bearer-${token}`);
// 4. 监听 WebSocket 连接状态
ws.onopen = () => {
console.log('WebSocket 连接成功(已通过 Sec-WebSocket-Protocol 传入 Token)');
// 连接成功后可发送消息
ws.send('客户端已连接');
};
ws.onmessage = (e) => {
console.log('收到后端消息:', e.data);
};
ws.onerror = (err) => {
console.error('WebSocket 连接失败/错误:', err);
};
ws.onclose = () => {
console.log('WebSocket 连接关闭');
};
})
</script>
关键说明:
-
第二个参数
Bearer-${token}可自定义格式(如token-${token}),只要后端解析时对应即可; -
无需引入任何第三方 Socket 库,纯原生 WebSocket,兼容性覆盖所有现代浏览器;
-
Token 从 localStorage 获取,实际开发中可结合 Vuex/Pinia 管理登录状态。
四、SpringBoot 后端核心实现(原生 WebSocket,无第三方库)
后端基于 SpringBoot 原生 WebSocket 实现,核心是在 WebSocket 握手阶段(OnOpen 方法),获取请求头 Sec-WebSocket-Protocol,解析出 Token 并完成鉴权。
4.1 WebSocket 配置类(开启 WebSocket 支持)
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* WebSocket 核心配置类
* 开启 SpringBoot 对 WebSocket 的支持
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
// 自动注册 @ServerEndpoint 注解的 WebSocket 服务端点
return new ServerEndpointExporter();
}
}
4.2 WebSocket 服务端点(核心:获取 Sec-WebSocket-Protocol)
java
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import javax.websocket.EndpointConfig;
import java.util.List;
/**
* WebSocket 服务端点
* 路径:/ws(与前端连接地址对应)
*/
@ServerEndpoint("/ws")
public class WebSocketServer {
/**
* 连接建立时触发(握手阶段)
* @param session WebSocket 会话
* @param config 端点配置,用于获取请求头
*/
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
// **** 获取请求头 Sec-WebSocket-Protocol ***
List<String> protocolList = config.getHandshakeRequest().getHeaders().get("Sec-WebSocket-Protocol");
if (protocolList != null && !protocolList.isEmpty()) {
// 取出子协议值(即前端传入的 Bearer-${token})
String protocol = protocolList.get(0);
// 解析 Token(与前端格式对应,截取 Bearer- 前缀)
String token = protocol.replace("Bearer-", "");
// Token 校验逻辑
boolean isAuth = checkToken(token);
if (!isAuth) {
// 鉴权失败,关闭连接
try {
session.close();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Token 鉴权失败,拒绝连接");
return;
}
// 鉴权成功
System.out.println("Token 鉴权成功,客户端连接建立,Token:" + token);
} else {
// 未获取到 Sec-WebSocket-Protocol(无 Token),关闭连接
try {
session.close();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("未传入 Token,拒绝连接");
}
}
/**
* 接收客户端消息
* @param message 客户端发送的消息
* @param session 会话
*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("收到客户端消息:" + message);
// 后端响应消息
session.getAsyncRemote().sendText("后端已收到消息:" + message);
}
/**
* 连接关闭时触发
*/
@OnClose
public void onClose() {
System.out.println("WebSocket 连接关闭");
}
/**
* 模拟 Token 校验
* @param token 前端传入的 Token
* @return 鉴权结果
*/
private boolean checkToken(String token) {
//模拟 Token 不为空即通过
return token != null && !token.isEmpty();
}
}
关键说明:
-
通过**
config.getHandshakeRequest().getHeaders()获取所有请求头,再通过 keySec-WebSocket-Protocol取出前端传入的子协议值;** -
无需引入第三方 WebSocket 依赖(SpringBoot 原生支持),仅需保证项目依赖中包含 spring-websocket(一般 SpringBoot -web 依赖已集成)。
五、避坑要点总结
-
不要试图在浏览器原生 WebSocket 中使用 headers 配置自定义请求头,官方不支持,会无效;
-
Sec-WebSocket-Protocol 的值不能包含中文、特殊符号,否则会导致连接失败;
-
前端传入的子协议格式,需与后端解析逻辑一致(如前端用 Bearer- 前缀,后端需对应截取);
-
后端必须在 OnOpen 方法(握手阶段)获取 Sec-WebSocket-Protocol,连接建立后无法再获取该请求头;
-
该方案纯原生实现,无第三方依赖,兼容性好,适合生产环境鉴权(比 URL 传参更安全)。