【WebSocket连接异常】前端使用WebSocket子协议传递token时,Java后端的正确打开方式!!!

文章目录

  • 1. 背景
  • 2. 代码实现和异常发现
  • 3. 解决异常
    • 3.1 从 URL入手
    • 3.2 从 WebSocket子协议的使用方式入手(真正原因)
  • 4. 总结(仍然存在的问题)

前言
本篇文章记录的是使用WebSocket进行双向通信时踩过的坑,希望能够帮助大家找到解决连接异常的正确方法。


1. 背景

本人在使用WebSocket实现“聊天室”的实时双向通信时(发消息、添加好友、处理好友请求等),一开始使用 cookie + session 的方式来管理用户的上下线情况,后来想引入 JWT,使用 token的方式来增强系统的可用性。这时我遇到了一个问题,大部分的接口都是使用 HTTP 协议的方式传输数据,因此我们可以将令牌放在 Header中用于身份校验;而 WebSocket进行双向通信时,前端无法直接在 header添加token。
经过网上查阅资料可知,有其他的方式可以在 HTTP升级为WebSocket时携带 token:(1)在 URL中追加 token(2)使用WebSocket的子协议传递 token。(通过抓包可以知道,token放在Header的 “Sec-WebSocket-Protocol” 中)

2. 代码实现和异常发现

考虑到 token直接暴露在 url 的安全性及优雅性等因素,我最终选择使用 WebSocket子协议来传递 token。以下是个人操作的过程及心路历程,若只想知道解决方法,可直接查看 3.2 从 WebSocket子协议的使用方式入手

前端代码如下:

var token = localStorage.getItem("token");
let websocket = new WebSocket("ws://" + location.host + "/WebSocketMessage", [token]);

对于后端来说,可以使用自定义拦截器来验证并处理token(存储token信息,以便后续在WebSocketSession中处理消息时使用),具体方法是自定义类继承 HandshakeInterceptor ,并重写它的两个方法。
建立连接前处理token的代码如下

@Component
public class SaveTokenInterceptor implements HandshakeInterceptor {// 握手前的操作,该方法返回 true 代表同意建立 WebSocket连接,false代表拒绝建立连接@Overridepublic boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {// HTTP协议 未正式升级为 WebSocket时,可以对 HTTP 报文中的信息进行一定的处理// 1. 从 header中获取子协议的token,若token为空、过期或非法的,则拒绝建立 WebSocket连接String token = request.getHeaders().getFirst("Sec-WebSocket-Protocol");Claims claims = JwtUtil.parseToken(token);		// 解析令牌if (claims == null) return false;// 2. 将 token 中的信息放入到 attributes属性中,后续 WebSoketSession可通过方法获取 attributes,进而获取里面存放的信息int id = (int) claims.get(Constant.CLAIM_USERID);String username = (String) claims.get(Constant.CLAIM_USERNAME);attributes.put(Constant.USER_TOKEN_KEY, new User(id, username, ""));	return true;}// 握手完成后的操作@Overridepublic void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {}
}

连接完成后,查看 WebSocketSession 是否能够正确拿到存储到 attributes 中的属性(通过第一个方法查看)

@Component
@Slf4j
public class TestWebSocket extends TextWebSocketHandler {// 这个方法会在 WebSocket建立成功后被自动调用@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {System.out.println("[WebSocketAPI] 连接成功!");// session.getAttributes() 得到一个 Map// 里面的元素为之前服务器Session存储的Attribute或放进去的其他自定义信息(上述处理token后存储的User对象)User user = (User) session.getAttributes().get(Constant.USER_TOKEN_KEY);log.info("[WebSocketAPI] afterConnectionEstablished, user: " + user);	// 验证是否将token信息放进去了if(user == null) {System.out.println("用户未登录!");return;}// 往 hash表 中存储对应客户端的WebSocket对象onlineUserManager.online(user.getId(), session);}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {// 这个方法是在 websocket 收到消息后被自动调用System.out.println("[WebSocketAPI] 收到消息! " + message.toString());}// 这个方法是在连接出现异常时被自动调用@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {System.out.println("[WebSocketAPI] 连接异常! " + exception.getMessage());User user = (User) session.getAttributes().get(Constant.USER_TOKEN_KEY);if(user == null) {return;}onlineUserManager.offline(user.getId(), session);}// 这个方法是在连接正常关闭后被自动调用@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {System.out.println("[WebSocketAPI] 关闭! " + status.toString());User user = (User) session.getAttributes().get(Constant.USER_TOKEN_KEY);if(user == null) {return;}onlineUserManager.offline(user.getId(), session);}
}

通过抓包及后端控制台日志观察上述过程:
在这里插入图片描述
在这里插入图片描述
可以发现:WebSocketSession 已经能够正确拿到 token里的信息,但是控制台也出现了WebSocket连接异常token校验失败两个异常现象。(通过浏览器控制台也可发现连接异常)

在这里插入图片描述


3. 解决异常

3.1 从 URL入手

首先,token校验失败原因比较简单,一般是在拦截器拦截 HTTP请求时发生,于是我通过抓包进行分析,但是令人感到奇怪的是所有 HTTP 请求均正常携带了 token,为什么会出现令牌解析不成功的情况呢?

经过一番思考,我决定在拦截器拦截请求时,通过 request获取所有经过拦截器的 HTTP请求的 URL,通过打印每个 HTTP 请求的URL及Header携带的 token 分析是否是前端 WebSocket 使用了子协议而导致被拦截器拦截,从而导致的异常。

@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String requestURI = request.getRequestURI();// 在方法执行前进行拦截,此处判断哪些方法可以被执行// 从 header中的token 判断用户是否登录String token = request.getHeader(Constant.USER_TOKEN_HEADER);System.out.println(token);System.out.println(requestURI);if (JwtUtil.parseToken(token) == null) {response.setStatus(401);return false;}return true;}
}

在这里插入图片描述

可以发现:上面的 HTTP 请求都符合预期,出现异常是使用 token 验证用户身份时,由浏览器默认发起的 favicon/ico(GET请求)并不会像其他 HTTP 请求一样,在其 Header 上携带 token,因此出现了令牌校验失败的情况。

通过上述偶然出现的异常也可以发现该程序上的一个问题,若一个 HTTP 的 Header 没有携带 token(即 token == null)就不需要进行令牌解析了,直接拦截即可。

因此只需将上述拦截器的拦截规则多做一个判断即可解决令牌解析的异常。(代码如下)

@Component
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String requestURI = request.getRequestURI();// 在方法执行前进行拦截,此处判断哪些方法可以被执行// 从 header中的token 判断用户是否登录String token = request.getHeader(Constant.USER_TOKEN_HEADER);if (token == null || JwtUtil.parseToken(token) == null) {response.setStatus(401);return false;}return true;}
}

3.2 从 WebSocket子协议的使用方式入手(真正原因)

由于抓包并不能找到问题出现的原因,因此我查阅了 WebSocket 子协议的相关使用方式发现:如果前端使用了子协议携带了 token,在 WebSocket连接完成后,返回的响应报文应该携带相同的子协议内容。

因此我立马通过抓包查看了响应报文:
在这里插入图片描述

可以发现,响应报文确实没有携带对应的 Header,为了验证 WebSocket连接异常的原因导致及上述说法的正确性,我对代码作出了如下修改:

@Component
public class SaveTokenInterceptor implements HandshakeInterceptor {// 握手前的操作@Overridepublic boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {// 1. 从 header中获取子协议的token,若token为空、过期或非法的,则拒绝建立 WebSocket连接String token = request.getHeaders().getFirst("Sec-WebSocket-Protocol");System.out.println("[SaveTokenInterceptor] beforeHandshake方法,token: " + token);Claims claims = JwtUtil.parseToken(token);if (claims == null) return false;// 2. 将 id 和 username 存入WebSocket的 attributes中int id = (int) claims.get(Constant.CLAIM_USERID);String username = (String) claims.get(Constant.CLAIM_USERNAME);attributes.put(Constant.USER_TOKEN_KEY, new User(id, username, ""));return true;}// 握手完成后的操作@Overridepublic void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {// 获取 Servlet 的 HttpServletRequest 和 HttpServletResponse 对象// httpRequest 可以获取 HTTP协议升级前 请求报文的信息,如 header中的键值对等// httpResponse 可以设置 HTTP响应 的相关信息,如状态码、ContentType、header信息等HttpServletRequest httpRequest = ((ServletServerHttpRequest) request).getServletRequest();HttpServletResponse httpResponse = ((ServletServerHttpResponse) response).getServletResponse();if (httpRequest.getHeader("Sec-WebSocket-Protocol") != null) {httpResponse.addHeader("Sec-WebSocket-Protocol", httpRequest.getHeader("Sec-WebSocket-Protocol"));}}
}

上述代码即在 WebSocket 连接完成后,针对响应增加了一个子协议的 header。

注意:无法直接通过 afterHandshake 方法参数的 ServerHttpResponse 修改响应内容,因为该接口并没有提供修改响应的方法。由于ServerHttpResponse是一个接口,通过源码我们可以发现:
ServletServerHttpResponse类 实现了该接口,且在Spring中 ServletServerHttpResponse 对 Servlet的 HttpServletResponse 类进行了封装,因此我们可以将 方法参数中的 response 强转为底层的实现类ServletServerHttpResponse,再通过 ServletServerHttpResponse 类中的方法获取封装的 HttpServletResponse 类,然后就可以使用该类设置响应报文的内容。
在这里插入图片描述
在这里插入图片描述

对代码作出上述修改后,运行程序的结果如下:
在这里插入图片描述
在这里插入图片描述

4. 总结(仍然存在的问题)

通过上述修改后,已经能够使用 token 验证用户身份,管理用户上下线情况,但仍然存在问题:

  1. 在使用 cookie-session 验证用户登录状态和上下线状态时,服务器重启重启会导致存储在内存的 session 消失,因此用户后续的任何请求都可能触发拦截器的拦截操作,需重新进行登录才能正常进行后续的操作。

而对于使用 token 来代替 cookie-session,虽然触发 HTTP 请求的操作能够做到 “用户无感知”,即服务器因某种原因重启后,用户不用二次登录依然可以完成操作;但对于使用 WebSocket 进行实时通信的消息转发、好友请求转发等功能来说,该程序使用 ConcurrentHashMap 来存储 WebSocketSession,服务器一旦重启,哈希表保存的登录信息就没了,这部分功能也因此直接“失效”了。

要想解决这个问题,可能需要引入 Redis 这样的中间件或使用其他的机制来实现 WebSocket 重连,以保证用户的使用体验。

  1. 当令牌达到过期时间,而用户没有触发发送 HTTP 请求的操作,而是进行发送消息这种操作,那么上述存储用户信息的方式则是错误的,因为这种做法虽然可以让接口代码只有小幅度修改,但会出现用户令牌虽然过期了但 ConcurrentHashMap 存储的 WebSocketSession 并不会被立即移除的情况,仍然能够进行消息发送(上一次操作停留在对话框界面)。

以上就是本篇文章的全部内容了,如果这篇文章对你有些许帮助,你的点赞、收藏和评论就是对我最大的支持。
另外,文章可能存在许多不足之处,也希望你可以给我一点小小的建议,我会努力检查并改进。

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

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

相关文章

基础拓扑学习

基础拓扑 有限集、可数集和不可数集 2.1 定义 考虑两个集 A A A和 B B B&#xff0c;他们的元素可以是任何东西。假定对于 A A A的每个元素 x x x&#xff0c;按照某种方式&#xff0c;与集 B B B的一个元素联系着&#xff0c;这个元素记作 f ( x ) f\left( x \right) f(x);那…

python学习笔记B-07:序列结构之列表--列表的常用函数和方法

以xx_函数名(列表名)的形式出现的是函数&#xff1b;以xx_列表名.xx_方法名的形式出现的是方法。 列表常用函数如下&#xff1a; len()&#xff1a;计算列表元素数量 max()&#xff1a;获取列表元素最大值 min():获取列表元素最小值 sum():计算列表中各元素之和 列表常用方法如…

windows下安装kibana

下载&#xff1a;https://www.elastic.co/cn/downloads/kibana 安装&#xff1a;https://www.elastic.co/guide/cn/kibana/current/install.html 安装好后&#xff0c;cd到kibana的bin目录&#xff0c;启动kibana.bat 然后访问localhost:5601

LeetCode450:删除二叉搜索树中的节点

题目描述 给定一个二叉搜索树的根节点 root 和一个值 key&#xff0c;删除二叉搜索树中的 key 对应的节点&#xff0c;并保证二叉搜索树的性质不变。返回二叉搜索树&#xff08;有可能被更新&#xff09;的根节点的引用。 一般来说&#xff0c;删除节点可分为两个步骤&#xf…

链表(C语言)

前言&#xff1a;前面几篇文章我们详细介绍了顺序表&#xff0c;以及基于顺序表来实现的通讯录。今天我们连介绍一下链表的下一个结构链表。那么链表和顺序表究竟有什么区别呢&#xff1f;他们两个的优缺点分别是什么。今天这篇文章就带大家了解一下链表。 目录 一.链表的概念…

瑞芯微RK3328(ROC-RK3328-PC)buildroot 开发QT的hello world

第一部分&#xff1a;编译rk3328 sdk 0. 环境 - EC-R3328PC&#xff08;ROC-RK3328-PC&#xff09; - ubuntu18&#xff08;100GB&#xff09; 1. 安装依赖 sudo apt-get updatesudo apt-get install repo git-core gitk git-gui gcc-arm-linux-gnueabihf u-boot-tools devi…

【系统移植三】uboot移植

开发板类型&#xff1a;emmc、7寸屏 1 NXP官方开发板uboot编译测试 1.1 获取源码 1&#xff09;源码路径&#xff1a;1、例程源码->4、NXP 官方原版 Uboot 和 Linux -> uboot-imx-rel_imx_4.1.15_2.1.0_ga.tar.bz2。 2&#xff09;将源码拷贝到ubuntu中的~/linux/IMX6…

Linux 目录结构与基础查看命令

介绍 目录结构如下 /bin&#xff1a;存放着用户最经常使用的二进制可执行命令&#xff0c;如cp、ls、cat等。这些命令是系统管理员和普通用户进行日常操作所必需的。 /boot&#xff1a;存放启动系统使用的一些核心文件&#xff0c;如引导加载器&#xff08;bootstrap loader…

采用C#.Net +JavaScript 开发的云LIS系统源码 二级医院应用案例有演示

采用C#.Net JavaScript 开发的云LIS系统源码 二级医院应用案例有演示 一、系统简介 云LIS是为区域医疗提供临床实验室信息服务的计算机应用程序&#xff0c;可协助区域内所有临床实验室相互协调并完成日常检验工作&#xff0c;对区域内的检验数据进行集中管理和共享&#xff0…

4*5的矩阵(C语言)

一、N-S流程图&#xff1b; 二、运行结果&#xff1b; 三、源代码&#xff1b; # define _CRT_SECURE_NO_WARNINGS # include <stdio.h>int main() {//初始化变量值&#xff1b;int i 0;int j 0;int result 0;//嵌套循环输出&#xff1b;for (i 1; i < 4; i){//列…

L2正则化——解释为什么可以减少模型的复杂度

L2正则化是一种用于机器学习模型的技术&#xff0c;旨在减少模型的复杂度&#xff0c;从而提高其泛化能力。在L2正则化中&#xff0c;通过添加一个惩罚项&#xff0c;模型的权重被迫保持较小的值&#xff0c;这有助于防止过拟合&#xff0c;即模型在训练数据上表现良好但在未见…

【Python】OPC UA模拟服务器实现

目录 服务器模拟1. 环境准备2. 服务器设置3. 服务器初始化4. 节点操作5. 读取CSV文件6. 运行服务器 查看服务器客户端总结 在工业自动化和物联网&#xff08;IoT&#xff09;领域&#xff0c;OPC UA&#xff08;开放平台通信统一架构&#xff09;已经成为一种广泛采用的数据交换…

单链表的基本操作实现:初始化、尾插法、头插法、输出单链表、求表长、按序号查找、按值查找、插入结点、删除结点。

1.参考学习博文&#xff08;写的相当好的文章&#xff09;&#xff1a; http://t.csdnimg.cn/AipNl 2.关于我的总结&#xff1a; 定义单链表&#xff1a; typedef struct LNode {Elemtype data;struct LNode* next; }LNode; data用来存放元素值&#xff0c;next用来指向后…

【算法】反转链表

本题来源---《反转链表》 题目描述&#xff1a; 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,2,1]示例 2&#xff1a; 输入&#xff1a;head [1,2] 输…

医学图像三维重建与可视化系统 医学图像分割 区域增长

医学图像的三维重建与可视化&#xff0c;这是一个非常有趣且具有挑战性的课题&#xff01;在这样的项目中&#xff0c;可以探索不同的医学图像技术&#xff0c;比如MRI、CT扫描等&#xff0c;然后利用这些图像数据进行三维重建&#xff0c;并将其可视化以供医生或研究人员使用。…

C++中的继承与多态

一、继承&#xff1a; 1.什么是继承&#xff1f; 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段&#xff0c;它允许程序员在保持原有类特性的基础上进行扩展&#xff0c;增加功能&#xff0c;这样产生新的类&#xff0c;称派生类。继承呈现了面向对象…

golang map总结

目录 概述 一、哈希表原理 哈希函数 哈希表和哈希函数的关系 哈希表的优势 哈希冲突 什么是哈希冲突 如何处理哈希冲突 链表法 开放寻址法 哈希表常见操作过程 存储数据 检索数据 删除数据 常用的哈希算法 哈希表的应用场景 二、golang map map的内部结构 h…

Docker Volume (存储卷)

什么是存储卷? 存储卷就是将宿主机的本地文件系统中存在的某个目录直接与容器内部的文件系统上的某一目录建立绑定关系。这就意味着&#xff0c;当我们在容器中的这个目录下写入数据时&#xff0c;容器会将其内容直接写入到宿主机上与此容器建立了绑定关系的目录。在宿主机上…

选课成绩管理系统

文章目录 员工管理系统一、项目演示二、项目介绍三、系统部分功能截图四、部分代码展示五、底部获取项目&#xff08;9.9&#xffe5;&#xff09; 员工管理系统 一、项目演示 课程管理系统 二、项目介绍 基于springbootvue的前后端分离选课成绩管理系统 该系统可做课程管理…

基础算法之二分算法

前言 本次博客&#xff0c;将要介绍二分算法的基本原理以及如何使用&#xff0c;深入浅出 二分可以针对整型以及浮点型接下来对其讲解希望对小白有所帮助吧 整型的二分法 一般要在一个数组中猜出一个数是否存在我们可以遍历一遍整个数组&#xff0c;判断是否存在&#xff0…