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

🎉🎉欢迎光临,终于等到你啦🎉🎉

🏅我是苏泽,一位对技术充满热情的探索者和分享者。🚀🚀

🌟持续更新的专栏《Spring 狂野之旅:从入门到入魔》 🚀

本专栏带你从Spring入门到入魔 

这是苏泽的个人主页可以看到我其他的内容哦👇👇

努力的苏泽icon-default.png?t=N7T8http://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实现多人聊天室(文字+语音+视频进阶:美颜 ,掉帧优化,掉线重连)

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

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

相关文章

MySQL-linux安装-万能RPM法

一、MySQL的Linux版安装 1、 CentOS7下检查MySQL依赖 1. 检查/tmp临时目录权限&#xff08;必不可少&#xff09; 由于mysql安装过程中&#xff0c;会通过mysql用户在/tmp目录下新建tmp_db文件&#xff0c;所以请给/tmp较大的权限。执行 &#xff1a; chmod -R 777 /tmp2. …

Aurora8b10b(2)上板验证

文章目录 前言一、AXI_Stream数据产生模块二、上板效果总结 前言 上一篇内容我们已经详细介绍了基于aurora8b10b IP核的设计&#xff0c;本文将基于此进一步完善并且进行上板验证。 设计思路及代码思路参考FPGA奇哥系列网课 一、AXI_Stream数据产生模块 AXIS协议是非常简单的…

Boost之Log: (3)、简单封装

设计目标: 1、每个Logging source对应一个目录&#xff0c;可以设置日志文件数&#xff0c;日志大小&#xff0c;目录名&#xff0c;文件名等 2、所有logging source日志目录都在一个根目录下。 3、可以动态创建和删除logging source 4、打印出日期时间和日志严重等级 示例代码…

前端试题2#记录

1、介绍以下CSS的盒子模型 盒子模型分为两种&#xff1a; &#xff08;1&#xff09;第一种是W3c标准的盒子模型&#xff08;标准盒模型&#xff09; width和height&#xff1a;内容的宽度、高度&#xff08;不是盒子的宽度、高度&#xff09;。padding&#xff1a;内边距。…

HarmonyOS入门-ArkTS学习(一)

1. 什么是ArkTS语言 学习之前&#xff0c;我们先初步了解下什么是ArkTS 官方指南这样介绍&#xff1a; ArkTS是TS的超集&#xff0c;ArkTS定义了声明式UI描述、自定义组件和动态扩展UI元素的能力&#xff0c;再配合ArkUI开发框架中的系统组件及其相关的事件方法、属性方法等共…

OpenHarmony实战:轻量级系统之子系统移植概述

OpenHarmony系统功能按照“系统 > 子系统 > 部件”逐级展开&#xff0c;支持根据实际需求裁剪某些非必要的部件&#xff0c;本文以部分子系统、部件为例进行介绍。若想使用OpenHarmony系统的能力&#xff0c;需要对相应子系统进行适配。 OpenHarmony芯片适配常见子系统列…

留学生在美国大学利用AI工具到底算不算作弊呢?

自2022年以来&#xff0c;美国大学就开启了一场AI作弊与反作弊大战 战场小至测验&#xff0c;大至申请 这场战争并没有一方胜利&#xff0c;作弊者心思费尽 校方反作弊弄得教授们苦不堪言 那么作为中国留学生该如何避免这场战役呢&#xff1f; 毕竟还是学业要紧呢…… 故事…

让六西格玛培训有效的三个步骤,拿走不谢!

近年来&#xff0c;六西格玛作为一种先进的质量管理方法&#xff0c;被众多企业视为提升产品质量、优化流程、减少浪费的利器。然而&#xff0c;如何使六西格玛培训真正落地生根&#xff0c;发挥出其应有的效果&#xff0c;成为了许多企业关注的焦点。本文&#xff0c;天行健Si…

每日五道java面试题之消息中间件MQ篇(二)

目录&#xff1a; 第一题. RabbitMQ的工作模式第二题. 如何保证RabbitMQ消息的顺序性&#xff1f;第三题. 消息如何分发&#xff1f;第四题. 消息怎么路由&#xff1f;第五题. 如何保证消息不被重复消费&#xff1f;或者说&#xff0c;如何保证消息消费时的幂等性&#xff1f; …

Android adb ime 调试输入法

目录 前言列出所有输入法仅列出输入法 id列出所有输入法的所有信息 启用/禁用 输入法启用输入法禁用输入法 切换输入法还原输入法 前言 安装多个输入法后&#xff0c;可以在设置里进行切换。 既然是开发&#xff0c;能用命令就就命令~ ime 帮助说明&#xff1a; ime <c…

目标检测、识别和语义分割的标注工具安装

计算机视觉 图像分类&#xff08;目标检测&#xff09;&#xff1a;一张图像中是否含某种物体物体定位&#xff08;目标检测与目标识别&#xff09;&#xff1a;确定目标位置和所属类别。语义分割&#xff08;目标分割和目标分类&#xff09;&#xff1a;对图像进行像素级分类…

推荐算法策略需求-rank model优化

1.pred_oobe (base) [rusxx]$ pwd /home/disk2/data/xx/icode/baidu/oxygen/rus-pipeline/pipeline-migrate/UserBaseActiveStatPipeline/his_session (base) [rusxx]$ sh test.sh 2. user_skill_history_dict_expt2包含userid [workxx]$ vim /home/work/xx/du-rus/du_rus_o…

Python 之 Flask 框架学习

毕业那会使用过这个轻量级的框架&#xff0c;最近再来回看一下&#xff0c;依赖相关的就不多说了&#xff0c;直接从例子开始。下面示例中的 html 模板&#xff0c;千万记得要放到 templates 目录下。 快速启动 hello world from flask import Flask, jsonify, url_forapp F…

【edge浏览器无法登录某些网站,以及迅雷插件无法生效的解决办法】

edge浏览器无法登录某些网站&#xff0c;以及迅雷插件无法生效的解决办法 edge浏览器无法登录某些网站&#xff0c;但chrome浏览器可以登录浏览器插件无法使用&#xff0c;比如迅雷如果重装插件重装浏览器重装迅雷后仍然出现问题 edge浏览器无法登录某些网站&#xff0c;但chro…

基于FPGA的SPI_FLASH程序设计

SPI_FLASH简介 spi_flash是一种通用存储器&#xff0c;也称为SPI NOR Flash或SPI Flash。它使用SPI&#xff08;Serial Peripheral Interface&#xff09;接口进行通信&#xff0c;可以通过串行方式读写数据。spi_flash的特点是工作电压低&#xff0c;体积小&#xff0c;读写速…

【单片机家电产品学习记录--红外线】

单片机家电产品学习记录–红外线 红外手势驱动电路&#xff0c;&#xff08;手势控制的LED灯&#xff09; 原理 通过红外线对管&#xff0c;IC搭建的电路&#xff0c;实现灯模式转换。 手势控制灯模式转换&#xff0c;详细说明 转载 1《三色调光LED台灯电路》&#xff0c…

矩阵空间秩1矩阵小世界图

文章目录 1. 矩阵空间2. 微分方程3. 秩为1的矩阵4. 图 1. 矩阵空间 我们以3X3的矩阵空间 M 为例来说明相关情况。目前矩阵空间M中只关心两类计算&#xff0c;矩阵加法和矩阵数乘。 对称矩阵-子空间-有6个3X3的对称矩阵&#xff0c;所以为6维矩阵空间上三角矩阵-子空间-有6个3…

【Turtle】海龟先生

什么是编程 计算机只懂0和1这样的语言&#xff0c;可是我们不懂&#xff0c;当我们希望 计算要能帮我们做事情的时候&#xff0c;该怎么办呢&#xff1f; 我们需要一种更简便的方法告诉计算机要做什么&#xff0c;所以人类发明了编程语言 利用计算机编程语言&#xff0c;我们…

硬件了解 笔记

motherboard的高低端区别在哪里&#xff1f; 核心&#xff1a;从单核变成双核&#xff0c;多核&#xff08;几核就是几个打工人&#xff09; 多线程&#xff1a;6核本来对应6个线程&#xff0c;但是多线程就是说6核对应12个线程 频率 主频&#xff1a;平时打工的速度 睿频&…

iNeuOS工业互联网操作系统,“低代码”表单开发应用过程(一)

iNeuOS工业互联网操作系统&#xff0c;“低代码”表单开发应用过程&#xff08;一&#xff09; 目 录 1. 概述... 2 2. “低代码”表单开发应用过程... 2 1. 概述 iNeuOS工业互联网操作系统“表单设计”功能经过升级后&#xff0c;能够适用于更多应用场景&…