【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,一经查实,立即删除!

相关文章

前端|babel升级

问题 项目不支持可选链调用过多的 babel 插件 步骤 基础包 dependencies “react-scripts”: “5.0.1” devDependencies “customize-cra”: “^1.0.0”,“react-app-rewired”: “^2.2.1”, 框架包 dependencies “react”: “16.13.1”,“react-dom”: “16.13.1”, …

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;内边距。…

Java常用API之Collections类解读

写在开头&#xff1a;本文用于作者学习Java常用API 我将官方文档中Collections类中所有API全测了一遍并打印了结果&#xff0c;日拱一卒&#xff0c;常看常新 addAll() 将所有指定元素添加到指定 collection 中 可以添加一个或多个元素 Testpublic void test_addAl…

HarmonyOS入门-ArkTS学习(一)

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

2024.2.26力扣每日一题——二叉搜索树的范围和

2024.2.26 题目来源我的题解方法一 深度搜索&#xff08;中序遍历&#xff09;方法二 广度搜索&#xff08;层序遍历&#xff09; 题目来源 力扣每日一题&#xff1b;题序&#xff1a;938 我的题解 方法一 深度搜索&#xff08;中序遍历&#xff09; 利用二叉搜索树中序遍历…

Python:使用Cator实现mysql数据库的CURD简化操作

目录 简介安装使用示例1、获取Database 对象2、Database对象3、Table操作 支持的占位符显示sql日志注意问题 Github: https://github.com/mouday/catorPypi: https://pypi.org/project/catorgitee: https://gitee.com/mouday/cator 简介 支持 mysql和sqlite数据库, 在现有连接…

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

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

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

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

软考 系统架构设计师系列知识点之软件架构风格(10)

接前一篇文章&#xff1a;软考 系统架构设计师系列知识点之软件架构风格&#xff08;9&#xff09; 所属章节&#xff1a; 第7章. 系统架构设计基础知识 第3节. 软件架构风格 相关试题 7. 某企业内部现有的主要业务功能已封装成为Web服务。为了拓展业务范围&#xff0c;需要将…

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

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

docker容器部署gitlab的runner的shell模式注册下job中无法使用docker指令

引言 现需通过gitlab-runner来构建jar部署的镜像,发现在job中无法使用docker指令,解决的过程中出现一系列异常,在此做个问题解决的记录。 内容 通过docker-compose部署 name: java-env services:env-gitlab-runner:restart: alwaysimage: env/gitlab-runner-java:latest…

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

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

前端导出文本内容为csv文件,excel乱码

原因&#xff1a;编码格式问题&#xff0c;需要改为utf-8 bom // Create blob with utf8-bom 编码 const createBlobWithBOM(data, mimeType)> {const bom [0xEF, 0xBB, 0xBF];const bomArray new Uint8Array(bom);const dataArray new TextEncoder().encode(data);const…

Android adb ime 调试输入法

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

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

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

计算机视觉无人驾驶技术:入门指南

I. 引言&#xff1a; 计算机视觉无人驾驶技术是一种基于计算机视觉和机器学习技术的自动化驾驶技术。它可以通过搭载各种传感器和摄像机&#xff0c;让车辆自主感知周围环境&#xff0c;实现尽可能自动化的驾驶操作。 这种技术具有重要性和优势&#xff0c;包括&#xff1a; …

js 本地缓存localStorage和sessionStorage 还有cookie 用法和区别

一、定义和使用 localStorage 和 sessionStorage 属性允许在浏览器中存储 key/value 对的数据。 localStorage 用于长久保存整个网站的数据&#xff0c;保存的数据没有过期时间&#xff0c;直到手动去删除。 sessionStorage 数据保存在当前会话中&#xff0c;该数据对象临时保…