Springboot+Websocket+JWT实现的即时通讯模块

      最后更新:2022-02-19 15:01

      场景

      目前做了一个接口:邀请用户成为某课程的管理员,于是我感觉有能在用户被邀请之后能有个立马通知他本人的机(类似微博、朋友圈被点赞后就有立马能收到通知一样),于是就闲来没事搞了一套。

      ​

      涉及技术栈

      • Springboot
      • Websocket 协议
      • JWT
      • (非必要)RabbitMQ 消息中间件

      Websocket 协议

      :star:推荐阅读: Websocket 协议简介

      WebSocket协议是基于TCP的一种新的 网络协议 。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。

      为什么使用Websocket?

      因为普通的http协议一个最大的问题就是:通信只能由客户端发起,服务器响应(半双工)****,而我们希望可以全双工通信。

      ​

      因此一句话总结就是:建立websocket(以下简称为ws)连接是为了让服务器主动向前端发消息,而无需等待前端的发起请求调用接口。

      业务逻辑

      我们现在有:

      • 用户A
      • 用户B
      • Springboot服务器
      • 场景:用户A调用接口邀请用户B成为课程成员
      • 涉及数据库MySQL的数据表:
        • course_member_invitation,记录课程邀请记录,其形式如下(忽略时间等列):
      id course_id account_id admin_id is_accepted bind_message_id
      邀请id 课程id 受邀用户id 邀请人id(因其本身为课程管理员) 受邀用户是否接受了邀请 绑定的消息id
      • course_message,记录消息记录,其形式如下(忽略时间等列):
      id type account_id source_id is_read is_ignored
      消息id 消息类型 收信人用户id 发信人用户id 是否已读 收信人是否忽略
      • (图中没有体现)course_message_type,记录消息类型,其形式如下
      id name description
      消息类型id 消息类型名称 描述
      • 涉及RabbitMQ(因不是重点,所以此处暂不讨论,最后一章叙述)

      业务步骤主要涉及两个方法addCourseMemberInvitation与sendMessage和一个组件CourseMemberInvitationListener,分别做:

      addCourseMemberInvitation:

      1. 用户A调用接口,邀请用户B成为某门课程的管理员
      2. Springboot服务器收到请求,将这一请求生成邀请记录、消息记录,写入下表:
      3. 写入DB后,调用sendMessage处理发送消息的业务。
      4. 将执行的结果返回给用户A

      sendMessage:

      1. 将消息记录放入RabbitMQ中对应的消息队列。

      CourseMemberInvitationListener:

      1. 持续监听其绑定的消息队列
      2. 一旦消息队列中有新消息,就尝试通过ws连接发送消息。

      在Springboot中配置Websocket

      • pom.xml文件
      • Websocket Server组件配置初步:com.xxxxx.course.webSocket.WebSocketServer

      几点说明:

      • onOpen方法:服务器与前端建立ws连接成功时自动调用。
      • sendInfo方法:是服务器通过用户id向指定用户发送消息的方法,其为静态公有方法,因此可供各service调用。调用的例子:
      • @ServerEndpoint注解:

      这么注解之后,前端只用发起ws://xxx.xxx:xxxx/ws/{uid}即可开启ws连接(或者wss协议,增加TLS), 比如前端js代码这么写:

      存在的问题

      一切看起来很顺利,我只要放个用户id进去,就可以想跟谁通讯就跟谁通讯咯!

      但设想一个场景, 我是小明,uid为250,我想找uid为520的小花聊天,理论上我只要发起ws://xxx.xxx:xxxx/ws/250请求与服务器连接,小花也发起ws://xxx.xxx:xxxx/ws/520与服务器建立ws连接,我们就能互发消息了吧!

      这时候出现了uid为1的小黄,他竟然想挖墙脚!?他竟然学过js,自己发了ws://xxx.xxx:xxxx/ws/520跟服务器建立ws连接,而小花根本不想和我发消息,所以实际上是小黄冒充了小花,把小花NTR了(实际上人家并不在乎:disappointed_relieved:),跟我愉快地聊天?!

      那怎么办啊?我怎么才能知道在跟我Websocket的究竟是美女小花还是黄毛小黄啊??!

      这就引入了JWT!

      JWT——JSON WEB TOKEN

      所以在调研了很多公开的解决方案,看到了可以改用令牌(token)来建立ws连接,同时验证用户身份(事实上一些其他接口也可以用令牌(token)来保证接口安全性)。

      js:

      业务逻辑

      为什么用JWT

      最初考虑的是用/ws/{userId}来建立ws连接,然后在后台拿session中的user来对比用户id,判断合法性。

      结果发现ws的session和http的session是不同的,还不好拿,可能得想办法把http的session存到redis或者DB(也可以存在内存中,只是可能又要消耗内存资源),在建立ws连接之前去拿出来验证合法性。后面查到了还有JWT这种好东西。

      JWT好在哪里?

      :star:推荐阅读: 什么是 JWT -- JSON WEB TOKEN ​

      我的总结:

      • token可以过期
      • 验证token可以不用存在redis或者DB或者内存,完全依赖算法即可
      • 只要从前端请求中拿到token,后端就可以根据封装好的算法验证这个token合不合法,有没有被篡改过(这点很重要),过期了没有
      • 可以将 用户id、用户名等非敏感数据一同封装到token中,后端拿到token后可以解码拿到数据 ,只要这个token合法,这些发来的数据就是可信的(小黄就算自己发明了token也不作数),是没有被篡改的(小黄就算把我小花的token偷走把用户id改成自己的也没用,后台可以算出来被改过),可以建立ws连接,调用websocket server进行通讯。教程
        JWT教程 整合到本项目中

      pom.xml

      token的前两个部分是由base64编码的,所以需要codec进行解码。

      ​

      实现一个JWT工具类

      目前基本当作工具使用

      com.xxxx.course.util.JWTUtil
      ​

      调整登陆 service 中,登陆时返回一个token

      com.xxxx.course.service.impl.AccountServiceImpl

      WebSocket 连接握手时进行身份验证

      之后前端只要携带token进行ws连接即可,写了个ws的配置类,继承了一个websocket连接的监听器ServerEndpointConfig.Configurator,进行token的验证。

      com.XXXXX.course.config.webSocket.WebSocketConfig

      这样,每次服务器建立ws连接前,都要验证token的合法性,仅仅通过JWTUtil.verify(token);即可!当token不合法,就会抛出异常。

      ​

      再配合重写websocket server的onOpen方法,应该就能进行身份可信的通信了!

      (非必须)RabbitMQ消息中间件

      教程

      • RabbitMQ 基本介绍+入门使用 : P14-P19 。看完这个视频 基本上能知道 RabbitMQ 是什么、怎么部署(推荐使用 docker ,学习时若使用centOS推荐使用7.x版本, 8.x我真的不会用)、怎么整合到springboot。

      为什么我要用RabbitMQ

      • 正经理由:
        • 可以将写DB与发送消息两件事情异步处理,这样响应会更快。
        • 未来可以拓展为集群
      • 真正理由:
        • 人傻
        • 闲的

      整合到本项目中

      pom.xml

      RabbitMQ 配置类

      • com.xxxx.course.config.rabbitMQ.RabbitMQConfig

      项目运行后,在RabbitMQ服务器中就会出现刚刚注册的队列与交换器(图是旧的,没有体现根据环境命名队列,但是其实做到了):

      课程管理成员邀请接口

      • com.scholat.course.service.impl.CourseMemberInvitationServiceImpl
        • 首先在接口中注入操作rabbitMQ的Bean
      • 在课程管理员业务代码中加入向rabbitMQ发送消息的逻辑
        • CourseMemberInvitationServiceImpl
      • courseMessageService中实现的sendMessage方法:
      • com.xxxx.course.rabbitMQ.listener.CourseMemberInvitationListener

      该类是用以监听_课程成员邀请_消息的,即是在rabbitMQ服务器建立的member_invitation队列。

      • com.xxxx.course.rabbitMQ.MessageHandler, 该类是用来处理监听事件的:

      这样做应该就可以用RabbitMQ了。

      ​

      总结

      本文的难点是ws的认证问题,虽然用超级好用的JWT解决了,但是随之而来的还有很多问题,比如:

      1. 注销登录等场景下 token 还有效
        与之类似的具体相关场景有:
        1. 退出登录;
        2. 修改密码;
        3. 服务端修改了某个用户具有的权限或者角色;
        4. 用户的帐户被删除/暂停。
        5. 用户由管理员注销;
      2. token 的续签问题
        token 有效期一般都建议设置的不太长,那么 token 过期后如何认证,如何实现动态刷新 token,避免用户经常需要重新登录?

      这些问题还是有待解决是:joy:

      本文就当记录一下自己的胡作非为吧:joy:

      总之,至少小花再也不怕被小黄NTR了

      本文由 在线网速测试 整理编辑,转载请注明出处。

          热门文章

          文章分类