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

news2024/12/23 3:11:41

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

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

🌟持续更新的专栏《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;//500
        if (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 {
    @Resource
    private 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 {
        @Override
        public 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;
    }

    @Override
    public 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  在前端代码中详见authorization
        String 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);
        //存在 保存用户信息到TheadLocal
        UserHolder.saveUser(user);
        System.out.println("保存用户"+user.getOpenId()+"信息到TheadLocal了");
        //刷新token有效期
        stringRedisTemplate.expire(key,15, TimeUnit.MINUTES);
        //放行
        return true;
    }

    @Override
    public 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是接收者的key
    private static ConcurrentMap<String, List<String>> messageMap = new ConcurrentHashMap<>();

    /**
     *
     * 连接建立成功调用的方法
     */
    @OnOpen
    public 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 userMessage
        List<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填的是自己的session
                this.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是被发送者的id
                messageMap.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"}]}
    }


    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(Session session, @PathParam("userId") String userId) {
        sessionMap.remove(userId);
        log.info("有一连接关闭,移除username={}的用户session, 当前在线人数为:{}", userId, sessionMap.size());
    }

    /**
     * 收到客户端消息后调用的方法
     * 后台收到客户端发送过来的消息
     * onMessage 是一个消息的中转站
     * 接受 浏览器端 socket.send 发送过来的 json数据
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public 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表示发送给哪个用户,比如 admin
        String 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 是 zhang
                jsonObject.set("text", text);  // text 同上面的text
                this.sendMessage(jsonObject.toString(), toSession);
                log.info("发送给用户username={},消息:{}", toUserId, jsonObject.toString());
            } else {
                log.info("发送失败,未找到用户username={}的session", toUserId);
            }
        }
    }

    @OnError
    public 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.coloradmin.cn/o/1565069.html

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

相关文章

CV论文--2024.4.3

1、Style Aligned Image Generation via Shared Attention 中文标题&#xff1a;共享注意力下的风格对齐图像生成 简介&#xff1a;大规模文本到图像&#xff08;T2I&#xff09;模型在创意领域迅速崭露头角&#xff0c;可以从文本提示中生成视觉上引人入胜的输出。然而&#…

【卫星家族】 | 高分六号卫星影像及获取

1. 卫星简介 高分六号卫星&#xff08;GF-6&#xff09;于2018年6月2日在酒泉卫星发射中心成功发射&#xff0c;是高分专项中的一颗低轨光学遥感卫星&#xff0c;也是我国首颗精准农业观测的高分卫星&#xff0c;具有高分辨率、宽覆盖、高质量成像、高效能成像、国产化率高等特…

C语言 | Leetcode C语言题解之第8题字符串转换整数atoi

题目&#xff1a; 题解&#xff1a; int myAtoi(char * s){int i0;int out0;int pol1;int lenstrlen(s);if(len0) return 0;while(s[i] ) i; //删除空格if(s[i]-){ //判断正负pol-1;i;}else if(s[i]){pol1;i;}else{pol1;}while(s[i]!\0){if(s[i]<0||s[i]>9){ /…

【Turtle】海龟先生

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

Transformer - 注意⼒机制

Transformer - 注意⼒机制 flyfish 计算过程 flyfish # -*- coding: utf-8 -*-import torch import torch.nn as nn import torch.nn.functional as F import os import mathdef attention(query, key, value, maskNone, dropoutNone):# query的最后⼀维的⼤⼩, ⼀般情况下就…

动态规划详解(Dynamic Programming)

目录 引入什么是动态规划&#xff1f;动态规划的特点解题办法解题套路框架举例说明斐波那契数列题目描述解题思路方式一&#xff1a;暴力求解思考 方式二&#xff1a;带备忘录的递归解法方式三&#xff1a;动态规划 推荐练手题目 引入 动态规划问题&#xff08;Dynamic Progra…

QT背景介绍

&#x1f40c;博主主页&#xff1a;&#x1f40c;​倔强的大蜗牛&#x1f40c;​ &#x1f4da;专栏分类&#xff1a;QT❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 目录 一、QT背景 1.1什么是QT 1.2QT的发展历史 1.3什么是框架、库 1.4QT支持的平台 1.5QT的优点 1.6QT的…

分布式锁 — Redisson 全面解析!

前言 分布式锁主要是解决集群&#xff0c;分布式下数据一致性的问题。在单机的环境下&#xff0c;应用是在同一进程下的&#xff0c;只需要保证单进程多线程环境中的线程安全性&#xff0c;通过 JAVA 提供的 volatile、ReentrantLock、synchronized 以及 concurrent 并发包下一…

JVM_垃圾收集器

GC垃圾收集器 文章目录 GC垃圾收集器GC垃圾回收算法和垃圾收集器关系GC算法主要有以下几种四种主要的垃圾收集器SerialParallelCMSG1垃圾收集器总结查看默认垃圾收集器 默认垃圾收集器有哪些各垃圾收集器的使用范围部分参数说明 新生代下的垃圾收集器并行GC(ParNew)并行回收GC&…

[Python GUI PyQt] PyQt5快速入门

PyQt5快速入门 PyQt5的快速入门0. 写在前面1. 思维导图2. 第一个PyQt5的应用程序3. PyQt5的常用基本控件和布局3.1 PyQt5的常用基本控件3.1.1 按钮控件 QPushButton3.1.2 文本标签控件 QLabel3.1.3 单行输入框控件 QLineEdit3.1.4 A Quick Widgets Demo 3.2 PyQt5的常用基本控件…

morkdown语法转微信公众号排版(免费)

morkdown语法转微信公众号排版&#xff08;免费&#xff09; 源码来自githab&#xff0c;有些简单的问题我都修复了。大家可以直接去找原作者的源码&#xff0c;如果githab打不开就从我下载的网盘里下载吧。 效果

在制定OKR的过程中,应该怎么确定目标O的来源或方向?

在制定OKR&#xff08;Objectives and Key Results&#xff0c;目标与关键成果&#xff09;的过程中&#xff0c;确定目标O的来源或方向是至关重要的一步。一个明确、合理的目标能够为团队指明方向&#xff0c;激发团队成员的积极性和创造力&#xff0c;进而推动公司的整体发展…

【嵌入式智能产品开发实战】(十五)—— 政安晨:通过ARM-Linux掌握基本技能【GNU C标准与编译器】

目录 GNU C 什么是C语言标准 C语言标准的内容 C语言标准的发展过程 1.K&R C 2.ANSI C 3.C99标准 4.C11标准 编译器对C语言标准的支持 编译器对C语言标准的扩展 政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 收录专栏: 嵌入式智能产品…

信息技术学院大数据技术专业开展专业实训周

四川城市职业学院讯&#xff08;信息技术学院 陈天伟&#xff09;日前&#xff0c;为提升学生的工匠精神和职业认知&#xff0c;信息技术学院邀请企业专家入驻眉山校区大数据实训基地&#xff0c;开展数据标识专业实训周。 数据标识是大数据专业的核心技术&#xff0c;数据标识…

在CentOS 7上安装Python 3.7.7

文章目录 一、实战步骤1. 安装编译工具2. 下载Python 3.7.7安装包3. 上传Python 3.7.7安装包4. 解压缩安装包5. 切换目录并编译安装6. 配置Python环境变量7. 使配置生效8. 验证安装是否成功 二、实战总结 一、实战步骤 1. 安装编译工具 在终端中执行以下命令 yum -y groupin…

24年大一训练一(东北林业大学)

前言&#xff1a; 周五晚上的训练赛&#xff0c;以后应该每两周都会有一次。 正文&#xff1a; Problem:A矩阵翻转&#xff1a; #include<bits/stdc.h> using namespace std; int a[55][55]; int main(){int n,m;while(cin>>n>>m){for(int i1;i<n;i){for…

1.Git是用来干嘛的

本文章学习于【GeekHour】一小时Git教程&#xff0c;来自bilibili Git就是一个文件管理系统&#xff0c;这样说吧&#xff0c;当多个人同时在操作一个文件的同时&#xff0c;很容易造成紊乱&#xff0c;git就是保证文件不紊乱产生的 包括集中式管理系统和分布式管理系统 听懂…

每日一题:用c语言写(输入n个数(n小于等于100),输出数字2的出现次数)

目录 一、要求 二、代码 三、结果 ​四、注意 一、要求 二、代码 #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int main() {//输入n个数&#xff08;n小于等于100&#xff09;&#xff0c;输出数字2的出现次数;int n[100] ;int num 0;int count 0;/…

加域报错:找不到网络路径

在尝试将计算机加入Windows域时&#xff0c;如果收到“找不到网络路径”的错误提示&#xff0c;可能的原因及解决方法如下&#xff1a; 网络连接问题&#xff1a;确保计算机与域控制器之间的物理网络连接是正常的&#xff0c;可以通过ping命令测试与域控制器的连通性。例如&…

【黑马头条】-day05延迟队列文章发布审核-Redis-zSet实现延迟队列-Feign远程调用

文章目录 昨日回顾今日内容1 延迟任务1.1 概述1.2 技术对比1.2.1 DelayQueue1.2.2 RabbitMQ1.2.3 Redis实现1.2.4 总结 2 redis实现延迟任务2.0 实现思路2.1 思考2.2 初步配置实现2.2.1 导入heima-leadnews-schedule模块2.2.2 在Nacos注册配置管理leadnews-schedule2.2.3 导入表…