引言
在前一篇文章中,实现了Spring Boot中集成Websocket服务,本篇文章简单介绍Vue3生态中,利用优秀的工具库@vueuse/core提供的useWebSocket组合式API,轻松集成WebSocket功能,并介绍3种前端携带token身份信息后端进行身份认证的方式。
Vue3项目集成Websocket
项目代码包不做过多阐述,直接使用npm create vue@latest 指令创建一个基本的项目,安装@vueuse/core,直接在组件中引入useWebsocket即可,useWebSocket文档地址。组件代码如下,基本代码是deepseek所写。
TypeScript
<template>
<div class="websocket-demo">
<div class="status-bar">
<span>连接状态:</span>
<span :class="statusClass">{{ statusText }}</span>
<button @click="toggleConnection" :disabled="connecting">
{{ isConnected ? "断开连接" : "连接" }}
</button>
</div>
<div class="message-area">
<h3>接收消息</h3>
<div class="message-list">
<div
v-for="(msg, index) in receivedMessages"
:key="index"
class="message-item"
>
{{ msg }}
</div>
<div v-if="receivedMessages.length === 0" class="empty-text">
暂无消息
</div>
</div>
</div>
<div class="send-area">
<input
v-model="message"
@keyup.enter="sendMessage"
placeholder="输入消息..."
:disabled="!isConnected"
/>
<button @click="sendMessage" :disabled="!isConnected">发送</button>
</div>
<div class="logs">
<h3>日志</h3>
<div class="log-list">
<div v-for="(log, index) in logs" :key="index" class="log-item">
{{ log }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from "vue";
import { useWebSocket } from "@vueuse/core";
// 建立websocket连接
const { data, close, open, send } = useWebSocket(
"ws://localhost:8089/ws",
{
autoConnect: false,
autoReconnect: false,
autoClose: true,
immediate: false,
heartbeat: {
message: "ping",
interval: 5000,
pongTimeout: 10000,
},
onMessage(ws, event) {
console.log("Received message:", event.data);
addLog(`收到消息: ${event.data}`);
},
onError(ws, event) {
console.error("WebSocket error:", event);
addLog("WebSocket 发生错误");
},
onConnected() {
isConnected.value = true;
addLog("WebSocket 连接成功");
},
onDisconnected() {
isConnected.value = false;
addLog("WebSocket 已断开连接");
}
},
);
// 响应式数据
const isConnected = ref(false);
const connecting = ref(false);
const receivedMessages = ref<string[]>([]);
const message = ref("");
const logs = ref<string[]>([]);
// 计算属性
const statusText = computed(() => {
if (connecting.value) return "连接中...";
return isConnected.value ? "已连接" : "未连接";
});
const statusClass = computed(() => ({
"status-connected": isConnected.value,
"status-disconnected": !isConnected.value && !connecting.value,
"status-connecting": connecting.value,
}));
// 添加日志
const addLog = (message: string) => {
const time = new Date().toLocaleTimeString();
logs.value.push(`[${time}] ${message}`);
// 限制日志数量
if (logs.value.length > 50) {
logs.value.pop();
}
};
// 切换连接状态
const toggleConnection = () => {
if (isConnected.value) {
close();
addLog("正在断开连接...");
} else {
open();
addLog("正在连接...");
}
};
// 发送消息
const sendMessage = () => {
addLog(`发送消息: ${message.value}`);
send(message.value);
message.value = "";
};
// 组件挂载时自动连接
onMounted(() => {
// 可选:自动连接
// connecting.value = true
// initWebSocket()
});
// 组件卸载时断开连接
onUnmounted(() => {});
</script>
<style scoped>
.websocket-demo {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.status-bar {
padding: 15px;
background: #f5f5f5;
border-radius: 8px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 15px;
}
.status-connected {
color: green;
font-weight: bold;
}
.status-disconnected {
color: red;
font-weight: bold;
}
.status-connecting {
color: orange;
font-weight: bold;
}
button {
padding: 8px 16px;
background: #4caf50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover:not(:disabled) {
background: #45a049;
}
button:disabled {
background: #cccccc;
cursor: not-allowed;
}
.message-area,
.send-area,
.logs {
margin-bottom: 20px;
}
.message-area h3,
.logs h3 {
margin-bottom: 10px;
color: #333;
}
.message-list,
.log-list {
border: 1px solid #ddd;
border-radius: 4px;
height: 200px;
overflow-y: auto;
padding: 10px;
background: #fafafa;
}
.message-item,
.log-item {
padding: 5px;
border-bottom: 1px solid #eee;
font-size: 14px;
word-break: break-all;
}
.message-item:last-child,
.log-item:last-child {
border-bottom: none;
}
.empty-text {
text-align: center;
color: #999;
padding: 20px;
}
.send-area {
display: flex;
gap: 10px;
}
.send-area input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.send-area input:focus {
outline: none;
border-color: #4caf50;
}
.log-list {
height: 150px;
font-family: monospace;
font-size: 12px;
}
.log-item {
color: #666;
}
</style>

携带身份信息的三种方式
一:使用URL参数(简单场景)
这是最直接的方式,客户端在连接URL中附上token,服务端从握手请求中解析参数。

Spring Boot服务端实现:
java
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
HttpServletRequest httpRequest = ((ServletServerHttpRequest) request).getServletRequest();
String token = httpRequest.getParameter("token");
// 验证token逻辑...
if (isValid(token)) {
// 此处可直接根据token获取用户信息,并保存在属性中
attributes.put("user", token);
return true;
}else {
log.error("token验证失败");
return false;
}
}

二:使用子协议(Subprotocol)
WebSocket支持在连接时指定子协议,客户端可在子协议中携带认证信息,服务端通过子协议处理器来验证。
HTTP Sec-WebSocket-Protocol 请求头和响应头用于 WebSocket 开启握手,以协商在通信中使用的子协议。这可以是一个广为人知的协议,例如 SOAP 或 WAMP,也可以是客户端和服务器理解的自定义协议。
在请求中,该头按偏好顺序指定一个或多个 WebSocke 子协议,web 应用程序希望使用这些协议。这些协议值可以作为多个头中的协议值添加,也可以作为逗号分隔的值添加到单个头中。
在响应中,它指定服务器选择的子协议。这必须是服务器从请求头提供的列表中支持的第一个子协议。
请求头由浏览器使用应用程序在 WebSocket() 的 protocols 参数中指定的值自动添加和填充。服务器选择的子协议在 WebSocket.protocol 中提供给 web 应用程序。

Spring Boot服务端实现:如果前端通过websocket连接时指定了Sec-WebSocket-Protocol,后端接收到连接后,必须原封不动的将Sec-WebSocket-Protocol头信息返回给前端,否则连接会抛出异常。
java
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
HttpServletRequest httpRequest = ((ServletServerHttpRequest) request).getServletRequest();
String token = httpRequest.getHeader("Sec-WebSocket-Protocol");
HttpServletResponse httpResponse = ((ServletServerHttpResponse) response).getServletResponse();
httpResponse.addHeader("Sec-WebSocket-Protocol", token);
// 可根据token进行登录信息查找 放入属性中
attributes.put("queryString", token);
return true;
}

三:使用特定的消息
在连接建立后,发送一个特定的消息,消息携带token,后端解析消息,进行身份的识别。

Spring Boot服务端实现:
java
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
JSONObject entries = JSONUtil.parseObj(payload);
if (entries.containsKey("type") && entries.getStr("type").equals("auth")){
String token = entries.getStr("token");
session.getAttributes().put("token", token);
return;
}
log.info("收到消息:{}", payload);
// 发送回复消息
String replyMessage = "服务器收到消息:" + payload;
session.sendMessage(new TextMessage(replyMessage));
}
总结
本文简要介绍了如何在Vue3中搭建Websocket服务,并介绍了3中与后端进行身份信息传输的方式。WebSocket 的身份验证核心在于握手机制------利用连接建立前的 HTTP 请求完成凭证传递。