【Spring实战项目】SpringBoot3整合WebSocket+拦截器实现登录验证!从原理到实战

🎉🎉欢迎光临,终于等到你啦🎉🎉
🏅我是苏泽,一位对技术充满热情的探索者和分享者。🚀🚀
🌟持续更新的专栏《Spring 狂野之旅:从入门到入魔》 🚀
本专栏带你从Spring入门到入魔
这是苏泽的个人主页可以看到我其他的内容哦👇👇
努力的苏泽
http://suzee.blog.csdn.net/
本文给大家带来的是SpringBoot整合WebSocket 实现一个简单的聊天功能 然后再进阶到语音的聊天 视频聊天
目录
在视频聊天的基础上 还要再实现 美颜、心跳检查掉线、掉帧优化。掉线重连等企业级业务需求
一、WebSocket概述:编辑
实现步骤
首先引入依赖
设置拦截器 自定义报错
这是我做的自定义类型 可以根据自己的修改
拦截器配置
拦截器实现
websocket服务实现
在视频聊天的基础上 还要再实现 美颜、心跳检查掉线、掉帧优化。掉线重连等企业级业务需求
一、WebSocket概述:

WebSocket是基于TCP协议的一种网络协议,它实现了浏览器与服务器全双工通信,支持客户端和服务端之间相互发送信息。在有WebSocket之前,如果服务端数据发生了改变,客户端想知道的话,只能采用定时轮询的方式去服务端获取,这种方式很大程度上增大了服务器端的压力,有了WebSocket之后,如果服务端数据发生改变,可以立即通知客户端,客户端就不用轮询去换取,降低了服务器的压力。目前主流的浏览器都已经支持WebSocket协议了。
WebSocket使用ws和wss作资源标志符,它们两个类似于http和https,wss是使用TSL的ws。主要有4个事件:
- onopen 创建连接时触发
- onclose 连接断开时触发
- onmessage 接收到信息时触发
- onerror 通讯异常时触发

实现步骤
首先引入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><exclusions><exclusion><groupId>org.junit.vintage</groupId><artifactId>junit-vintage-engine</artifactId></exclusion></exclusions></dependency><dependency><!-- websocket --><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency><dependency><!-- fastjson --><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.47</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency>
设置拦截器 自定义报错
@Slf4j
@RestControllerAdvice
public class WebExceptionAdvice {@ExceptionHandler(RuntimeException.class)public ResponseEntity<Result> handleRuntimeException(HttpServletRequest request, RuntimeException e) {log.error(e.toString(), e);Result result = Result.fail(e.getMessage());HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;//500if (e instanceof UnAuthorException) {//这个是拦截器报错才设置的状态码status = HttpStatus.UNAUTHORIZED;//401}ResponseEntity<Result> resultResponseEntity = new ResponseEntity<>(result, status);log.error(resultResponseEntity.toString());return resultResponseEntity;}
}
这是我做的自定义类型 可以根据自己的修改
public class UnAuthorException extends RuntimeException {public UnAuthorException(String message) {super(message);}
}
拦截器配置
@Configuration
public class MvcConfig implements WebMvcConfigurer {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Override//添加拦截器 InterceptorRegistry registry 拦截器的注册器 excludePathPatterns排除不需要的拦截的路径// 只要跟登录无关就不需要拦截 拦截器的作用只是校验登录状态public void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new LoginInterceptor()).excludePathPatterns("/index/**","/user/wechat/login","/user/zfb/login",//...这里自己去设置 不想被拦截的页面 剩下的就是被拦截的).order(1);
// order是设置先后
// 刷新token的拦截器registry.addInterceptor(new RefreshTokeninterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);}
}
拦截器实现
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//1.判断是否需要拦截(ThreadLocal中是否有用户)if (UserHolder.getUser() == null&&ListenerHolder.getListener()==null) {System.out.println("拦截器报错啦!!!");//response.getHeader("erro");throw new UnAuthorException("用户未登录");}return true;}
}
/*/***@author suze*@date 2023-10-25*@time 15:23**/
public class RefreshTokeninterceptor implements HandlerInterceptor {//而MvcConfig中使用了 LoginInterceptor 所以我们要去到MvcConfig进行注入private StringRedisTemplate stringRedisTemplate;//因为这个类不是spring boot构建的,而是手动创建的类,所以依赖注入不能用注解来注入,要我们手动使用构造函数来注入这个依赖public RefreshTokeninterceptor(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String token2=request.getHeader("token2");String ListenerKey = LOGIN_LISTENER_KEY + token2;//这里的倾听者信息是在倾听者登录的函数里面把倾听者信息录入进去String LisStr = stringRedisTemplate.opsForValue().get(ListenerKey);if(LisStr== null || LisStr.isEmpty()){System.err.println("倾听者token为空");}else {Listener listener = JSON.parseObject(LisStr, Listener.class);ListenerHolder.saveListener(listener);stringRedisTemplate.expire(ListenerKey,15, TimeUnit.MINUTES);return true;}//获取请求头中的token 在前端代码中详见authorizationString token = request.getHeader("token");if(StrUtil.isBlank(token)){//判断是否为空System.err.println("token为空");return true;}// 基于token获取Redis用户String key =LOGIN_USER_KEY+token;String userstr = stringRedisTemplate.opsForValue().get(key);//System.err.println("基于token获取Redis用户:"+userstr);//判断用户是否存在 不存在的话就查询是否是倾听者的情况if(userstr== null || userstr.isEmpty()){System.err.println("用户为空");return true;}// 将查询到的user的json字符串转化为user对象User user = JSON.parseObject(userstr, User.class);//存在 保存用户信息到TheadLocalUserHolder.saveUser(user);System.out.println("保存用户"+user.getOpenId()+"信息到TheadLocal了");//刷新token有效期stringRedisTemplate.expire(key,15, TimeUnit.MINUTES);//放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {//移除用户UserHolder.removeUser();ListenerHolder.removeListener();}
}
根据自己需求 删掉一些我这边业务的部分 不删也行 也能用 就是有点慢
websocket服务实现
@ServerEndpoint(value = "/imserver/{userId}")
@Component
public class WebSocketServer {private static final Logger log = LoggerFactory.getLogger(WebSocketServer.class);/*** 记录当前在线连接数*/public static final Map<String, Session> sessionMap = new ConcurrentHashMap<>();//public static final Map<String, Session> UserMap = new ConcurrentHashMap<>();这里没有需要知道对方名字的需求 所以不需要加 需要再加/*** <<<<<<< HEAD* 设置为静态的 公用一个消息map ConcurrentMap为线程安全的map HashMap不安全*///这里的messageMap存的是某用户已经离线 他离线后收到的消息的集合 所以这里的key是接收者的keyprivate static ConcurrentMap<String, List<String>> messageMap = new ConcurrentHashMap<>();/**** 连接建立成功调用的方法*/@OnOpenpublic void onOpen(Session session, @PathParam("userId") String userId) {sessionMap.put(userId, session);// stringRedisTemplate.opsForList().log.info("有新用户加入,userId={}, 当前在线人数为:{}", userId, sessionMap.size());JSONObject result = new JSONObject();JSONArray array = new JSONArray();result.set("users", array);for (Object key : sessionMap.keySet()) {JSONObject jsonObject = new JSONObject();jsonObject.set("userId", key);// {"userId": "aysgduiehfiuew", "userId": "admin"}array.add(jsonObject);}//这里得到的是该用户的历史记录map userMessageList<String> userMessage = messageMap.get(userId);//载入历史记录 这个过程相当于重新把消息发给自己if (userMessage!=null) {for (int i = userMessage.size() - 1; i >= 0; i--) {String message = userMessage.get(i);//这里的session的作用是告诉sendMessage发给谁 这里是要加载自己错过的历史消息// 所以是把历史记录发给自己 所以toSession填的是自己的sessionthis.sendMessage(message, session);
// Thread.sleep(10000);}messageMap.remove(userId);}
// {"users": [{"userId": "zhang"},{ "userId": "admin"}]}sendAllMessage(JSONUtil.toJsonStr(result)); // 后台发送消息给所有的客户端}/*** 服务端发送消息给客户端*/private void sendMessage(String message, Session toSession) {try {log.info("服务端给客户端[{}]发送消息{}", toSession.getId(), message);toSession.getBasicRemote().sendText(message);//String from = JSONUtil.parseObj(message).getStr("from");if (!messageMap.get(toSession.getId()).isEmpty()) {List<String> list = messageMap.get(toSession.getId());log.info("有待发送的消息,继续存储");list.add(message);//toSession是被发送者的idmessageMap.put(toSession.getId(), list);return;} else {List<String> list = new ArrayList<>();//该用户发的离线消息的集合list.add(message);messageMap.put(toSession.getId(), list);log.info("用户不在线保存信息");return;}} catch (Exception e) {log.error("服务端发送消息给客户端失败", e);}// {"users": [{"userId": "zhang"},{ "userId": "admin"}]}}/*** 连接关闭调用的方法*/@OnClosepublic void onClose(Session session, @PathParam("userId") String userId) {sessionMap.remove(userId);log.info("有一连接关闭,移除username={}的用户session, 当前在线人数为:{}", userId, sessionMap.size());}/*** 收到客户端消息后调用的方法* 后台收到客户端发送过来的消息* onMessage 是一个消息的中转站* 接受 浏览器端 socket.send 发送过来的 json数据* @param message 客户端发送过来的消息*/@OnMessagepublic void onMessage(String message, Session session, @PathParam("userId") String userId) {log.info("服务端收到用户username={}的消息:{}", userId, message);JSONObject obj = JSONUtil.parseObj(message);String toUserId = obj.getStr("to"); // to表示发送给哪个用户,比如 adminString text = obj.getStr("text"); // 发送的消息文本 hello//建立一个数组 把每一次的都装进去 然后下面//TODO 这里要写 一个缓存历史记录的方法来处理 除了test123 是用于心跳的 就不用缓存if(!toUserId.equals("test123")){Session toSession = sessionMap.get(toUserId); // 根据 to userId来获取 session,再通过session发送消息文本if (toSession != null) {// 服务器端 再把消息组装一下,组装后的消息包含发送人和发送的文本内容// {"from": "zhang", "text": "hello"}JSONObject jsonObject = new JSONObject();jsonObject.set("from", userId); // from 是 zhangjsonObject.set("text", text); // text 同上面的textthis.sendMessage(jsonObject.toString(), toSession);log.info("发送给用户username={},消息:{}", toUserId, jsonObject.toString());} else {log.info("发送失败,未找到用户username={}的session", toUserId);}}}@OnErrorpublic void onError(Session session, Throwable error) {log.error("发生错误");error.printStackTrace();}/*** 服务端发送消息给所有客户端*/private void sendAllMessage(String message) {try {for (Session session : sessionMap.values()) {log.info("服务端给客户端[{}]发送消息{}", session.getId(), message);session.getBasicRemote().sendText(message);}} catch (Exception e) {log.error("服务端发送消息给客户端失败", e);}}
}
这里再写视频聊天就太多了 打算放到下一篇专门来写 如果感兴趣的朋友可以私信找我拿项目 或者关注我下一篇专门讲解
给个三连吧兄弟们 制作不易
WebRTC实现多人聊天室(文字+语音+视频进阶:美颜 ,掉帧优化,掉线重连)
相关文章:
【Spring实战项目】SpringBoot3整合WebSocket+拦截器实现登录验证!从原理到实战
🎉🎉欢迎光临,终于等到你啦🎉🎉 🏅我是苏泽,一位对技术充满热情的探索者和分享者。🚀🚀 🌟持续更新的专栏《Spring 狂野之旅:从入门到入魔》 &a…...
第二证券|政策利好不断,工业母机概念爆发,华东数控等涨停
工业母机概念10日盘中大幅走高,截至发稿,恒进感应、宏德股份、华东重机、华东数控等涨停,凯腾精工涨超20%,创世纪涨逾11%,华辰配备、盘古智能涨超9%,博亚精工涨逾8%。 音讯面上,工业和信息化部…...
Thinkphp5萤火商城B2C小程序源码
源码介绍 Thinkphp5萤火商城B2C小程序源码,是一款开源的电商系统,为中小企业提供最佳的新零售解决方案。采用稳定的MVC框架开发,执行效率、扩展性、稳定性值得信赖。 环境要求 Nginx/Apache/IIS PHP5.4 MySQL5.1 建议使用环境ÿ…...
PostgreSQL介绍
PostgreSQL是一个高度先进的对象关系型数据库管理系(ORDBMS),其起源可以追溯到1986年,最初是加州大学伯克利分校计算机系的一个项目,名为POSTGRES。它是从Ingres项目演变而来的,目的是克服当时关系数据库系…...
简析数据安全保护策略中的十个核心要素
数据显示,全球企业组织每年在数据安全防护上投入的资金已经超过千亿美元,但数据安全威胁态势依然严峻,其原因在于企业将更多资源投入到数据安全能力建设时,却忽视了这些工作本身的科学性与合理性。因此,企业在实施数据…...
Python+Django+Html河道垃圾识别网页系统
程序示例精选 PythonDjangoHtml河道垃圾识别网页系统 如需安装运行环境或远程调试,见文章底部个人QQ名片,由专业技术人员远程协助! 前言 这篇博客针对《PythonDjangoHtml河道垃圾识别网页系统》编写代码,代码整洁,规…...
BUUCTF:BUU UPLOAD COURSE 1[WriteUP]
构造一句话PHP木马 <?php eval(system($_POST[shell])); ?> 利用eval函数解析$shell的值使得服务器执行system命令 eval函数是无法直接执行命令的,只能把字符串当作php代码解析 这里我们构造的木马是POST的方式上传,那就用MaxHacKBar来执行 …...
从零开始学习:如何使用Selenium和Python进行自动化测试?
安装selenium 打开命令控制符输入:pip install -U selenium 火狐浏览器安装firebug:www.firebug.com,调试所有网站语言,调试功能 Selenium IDE 是嵌入到Firefox 浏览器中的一个插件,实现简单的浏览器操 作的录制与回…...
【linux基础】bash脚本的学习:定义变量及引用变量、统计目标目录下所有文件行数、列数
假设目的:统计并输出指定文件夹下所有文件行数 单个文件可以用 wc -l ;多个文件,可以用通配符 / 借助bash脚本 1.定义变量名,使用引号 a"bestqc.com.map" b"Anno.variant_function" c"enrichment/GOe…...
算法四十天-删除排序链表中的重复元素
删除排序链表中的重复元素 题目要求 解题思路 一次遍历 由于给定的链表是排好序的,因此重复的元素在链表中的出现的位置是连续的,因此我们只需要对链表进行一次遍历,就可以删除重复的元素。 具体地,我们从指针cur指向链表的头节…...
Linux-等待子进程
参考资料:《Linux环境编程:从应用到内核》 僵尸进程 进程退出时会进行内核清理,基本就是释放进程所有的资源,这些资源包括内存资源、文件资源、信号量资源、共享内存资源,或者引用计数减一,或者彻底释放。…...
【LeetCode热题100】【二叉树】二叉树的最大深度
题目链接:104. 二叉树的最大深度 - 力扣(LeetCode) 最大深度等于左子树的最大深度和右子树的最大深度中的较大者加一 class Solution { public:int maxDepth(TreeNode *root) {if (!root)return 0;return max(maxDepth(root->left), max…...
想做产品经理,应该选择什么专业?
产品经理作为互联网公司的核心职位,一直以来备受关注。随着互联网的不断发展,产品经理的需求也越来越高,很多人都想要了解哪些专业适合做产品经理。本文将为大家介绍几个适合做产品经理的专业。 1、心理学相关专业 C端产品工作的本源&#x…...
[机器学习Day 1~3
[机器学习]Day 1~3 数据预处理第1步:导入库第2步:导入数据集第3步:处理丢失数据第4步:解析分类数据创建虚拟变量 第5步:拆分数据集为训练集合和测试集合第6步:特征量化 简单线性回归模型第一步:…...
Day106:代码审计-PHP原生开发篇文件安全上传监控功能定位关键搜索1day挖掘
目录 emlog-文件上传&文件删除 emlog-模板文件上传 emlog-插件文件上传 emlog-任意文件删除 通达OA-文件上传&文件包含 知识点: PHP审计-原生开发-文件上传&文件删除-Emlog PHP审计-原生开发-文件上传&文件包含-通达OA emlog-文件上传&文件…...
数码视讯Q7盒子刷armbian遇到的坑之二
继续,nand的q7 搜遍全网,这个盒子能用的安卓映像有两个,一个本站付费下载的那个,另一个是20191218-Q7-nand-4.4.2-root-twrp-Milton这个映像(具体地址自己搜索吧)。第一个需要license,需要自己…...
vue2 使用vue-org-tree demo
1.安装 npm i vue2-org-tree npm install -D less-loader less安装 less-loader出错解决办法,直接在package.json》devDependencies下面加入less和less-loader版本,然后执行npm i ,我用的nodejs版本是 16.18.0,“webpack”: “^4…...
【数据结构】考研真题攻克与重点知识点剖析 - 第 7 篇:查找
前言 本文基础知识部分来自于b站:分享笔记的好人儿的思维导图与王道考研课程,感谢大佬的开源精神,习题来自老师划的重点以及考研真题。此前我尝试了完全使用Python或是结合大语言模型对考研真题进行数据清洗与可视化分析,本人技术…...
【数仓】DataX 通过SpringBoot项目自动生成 job.json 文件
相关文章 【数仓】基本概念、知识普及、核心技术【数仓】数据分层概念以及相关逻辑【数仓】Hadoop软件安装及使用(集群配置)【数仓】Hadoop集群配置常用参数说明【数仓】zookeeper软件安装及集群配置【数仓】kafka软件安装及集群配置【数仓】flume软件安…...
注解式 WebSocket - 构建 群聊、单聊 系统
目录 前言 注解式 WebSocket 构建聊天系统 群聊系统(基本框架) 群聊系统(添加昵称) 单聊系统 WebSocket 作用域下无法注入 Spring Bean 对象? 考虑离线消息 前言 很久之前,咱们聊过 WebSocket 编程式…...
51c自动驾驶~合集58
我自己的原文哦~ https://blog.51cto.com/whaosoft/13967107 #CCA-Attention 全局池化局部保留,CCA-Attention为LLM长文本建模带来突破性进展 琶洲实验室、华南理工大学联合推出关键上下文感知注意力机制(CCA-Attention),…...
DockerHub与私有镜像仓库在容器化中的应用与管理
哈喽,大家好,我是左手python! Docker Hub的应用与管理 Docker Hub的基本概念与使用方法 Docker Hub是Docker官方提供的一个公共镜像仓库,用户可以在其中找到各种操作系统、软件和应用的镜像。开发者可以通过Docker Hub轻松获取所…...
无法与IP建立连接,未能下载VSCode服务器
如题,在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈,发现是VSCode版本自动更新惹的祸!!! 在VSCode的帮助->关于这里发现前几天VSCode自动更新了,我的版本号变成了1.100.3 才导致了远程连接出…...
学校招生小程序源码介绍
基于ThinkPHPFastAdminUniApp开发的学校招生小程序源码,专为学校招生场景量身打造,功能实用且操作便捷。 从技术架构来看,ThinkPHP提供稳定可靠的后台服务,FastAdmin加速开发流程,UniApp则保障小程序在多端有良好的兼…...
家政维修平台实战20:权限设计
目录 1 获取工人信息2 搭建工人入口3 权限判断总结 目前我们已经搭建好了基础的用户体系,主要是分成几个表,用户表我们是记录用户的基础信息,包括手机、昵称、头像。而工人和员工各有各的表。那么就有一个问题,不同的角色…...
2021-03-15 iview一些问题
1.iview 在使用tree组件时,发现没有set类的方法,只有get,那么要改变tree值,只能遍历treeData,递归修改treeData的checked,发现无法更改,原因在于check模式下,子元素的勾选状态跟父节…...
Java入门学习详细版(一)
大家好,Java 学习是一个系统学习的过程,核心原则就是“理论 实践 坚持”,并且需循序渐进,不可过于着急,本篇文章推出的这份详细入门学习资料将带大家从零基础开始,逐步掌握 Java 的核心概念和编程技能。 …...
华为云Flexus+DeepSeek征文|DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建
华为云FlexusDeepSeek征文|DeepSeek-V3/R1 商用服务开通全流程与本地部署搭建 前言 如今大模型其性能出色,华为云 ModelArts Studio_MaaS大模型即服务平台华为云内置了大模型,能助力我们轻松驾驭 DeepSeek-V3/R1,本文中将分享如何…...
在WSL2的Ubuntu镜像中安装Docker
Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包: for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...
【碎碎念】宝可梦 Mesh GO : 基于MESH网络的口袋妖怪 宝可梦GO游戏自组网系统
目录 游戏说明《宝可梦 Mesh GO》 —— 局域宝可梦探索Pokmon GO 类游戏核心理念应用场景Mesh 特性 宝可梦玩法融合设计游戏构想要素1. 地图探索(基于物理空间 广播范围)2. 野生宝可梦生成与广播3. 对战系统4. 道具与通信5. 延伸玩法 安全性设计 技术选…...
