Hoody's Blog
Springboot 接入Websocket (with Shiro)

废话

博客功能基本算能用了 昨天做了图片插入和上传 下一步打算给博客增加评论搜索部分 大概在这个事情完成之后吧

目前 打算给我的ECS服务器做一个状态监控

前端准备使用Echarts做图表 服务器信息通过云服务器 ECS API 获取,然后再自己加一些信息 然后通过websocket 发送状态数据

Echarts 已经比较熟悉了,有再项目中使用过 Websocket 也有在项目中使用过,不过是在Grails Framework


进入正题

Springboot 配置 Websocket

根据官方指导页面Using WebSocket to build an interactive web application 网上大量文章都是在使用过时的配置方式,springboot 已经帮助我们做了很多配置 推荐优先查看官方文档

我的Springboot版本为2.1.4.RELEASE

第一步 添加依赖

pom.xml

<dependencies>
    //others dependency
    <!-- 添加依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
</dependencies>

第二步 创建接收前端消息类

WebsocketMessage.groovy ps:一定需要set/get方法

class WebsocketMessage {
    /** 我使用的是Groovy 
    *  也可以定义 String data 
    *  消息内容
    */    
    def data
    /** 发送的目的地 */
    String destination

    def getData() {
        return data
    }

    void setData(data) {
        this.data = data
    }

    String getDestination() {
        return destination
    }

    void setDestination(String destination) {
        this.destination = destination
    }
}

第三步 创建Websocket控制器

MonitorController.groovy

@Controller
class MonitorController {
    @MessageMapping("/hello")  //响应前端发送"/app/hello"地址
    @SendTo("/topic/helloEcho") // 告诉springboot将方法返回值通过websocket发送到 "/topic/helloEcho"地址
    public ResponseData helloEcho(WebsocketMessage message) throws Exception {
    /** 我这里是返回了自定义的响应对象,并放入了用户发送来的消息 */
        return new ResponseData(data: "echo:${message.getData()}")
    }
}

第四步 创建Websocket 配置类

WebSocketConfig.java 特别注意 registry.addEndpoint("/hoody-websocket").setAllowedOrigins("*").withSockJS(); 开发环境我使用了通配符, 正式环境应该传入 允许的源 可以通过profile控制

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    /** 允许使用topic,并且所有主题地址前缀为"/app" */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }
    /**
     * 添加一个/hoody-websocket端点,客户端就可以通过这个端点来进行连接;
     * withSockJS作用是添加SockJS支持,
     * setAllowedOrigins(String... var1) 指定可以跨域访问的地址
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/hoody-websocket").setAllowedOrigins("*").withSockJS();
    }
}

第五步 前端Vue 使用 StompSockJS 访问

<script>
import Stomp from 'stompjs'
import SockJS from 'sockjs-client'

export default {
  name: 'websocket',
  data() {
    return {
      stompClient: null,
      timer: null,
    }
  },
  mounted() {
    this.initWebSocket()
  },
  beforeDestroy: function() {
    /** 页面离开时断开连接,清除定时器 */
    this.disconnect()
    clearInterval(this.timer)
  },
  methods: {
    /** 绑定 按钮发送 */
    clickBtn() {
      this.sendWsMessage("Hi,world")
    },
    sendWsMessage(msg) {
    /** 向'/app/hello' 发送 消息,需要将对象转换为JSON 字符串
      this.stompClient.send('/app/hello', {},
        JSON.stringify({ data: msg }),
      )
    },
    /** 收到消息处理 */
    onMessage(msg) {
      this.$message.success('收到消息:' + msg) // msg.body存放的是服务端发送给我们的信息
    },
    /** 初始化Socket连接 */
    initWebSocket() {
      this.connection()
      const that = this
      // 断开重连机制,尝试发送消息,捕获异常发生时重连 10秒一次
      this.timer = setInterval(() => {
        try {
          that.stompClient.send('test')
        } catch (err) {
          that.$message.warning('断线了: ' + err)
          that.connection()
        }
      }, 10000)
    },
    connection() {
      // 建立连接对象
      const socket = new SockJS('/api/hoody-websocket')
      // 获取STOMP子协议的客户端对象
      this.stompClient = Stomp.over(socket)
      // 定义客户端的认证信息,按需求配置
      // 向服务器发起websocket连接
      const that = this
      this.stompClient.connect({ 'X-Token': that.$store.getters.token }, () => {
        this.stompClient.subscribe('/topic/helloEcho', (msg) => { // 订阅服务端提供的某个topic
          that.onMessage(msg.body)
          msg.ack()
        })
        // 用户加入接口
      }, (err) => {
        // 连接发生错误时的处理函数
        that.$message.error(err)
      })
    }, // 连接 后台
    disconnect() {
      if (this.stompClient) {
        this.stompClient.disconnect()
      }
    } // 断开连接
  }
}
</script>

参考:SockJS-client API firejq 用户 CSDN上的STOMP 客户端 API 整理

现在可以愉快的访问了

第七步 了解shiro-spring-boot-web-starter 帮我们做了的事

在浏览器开发者工具 的Console 可以看到连接成功的信息

>>> CONNECT
X-Token:4dead694-eb84-496f-ba93-802b6fcbe939
accept-version:1.1,1.0
heart-beat:10000,10000
------------------------------------------------------
<<< CONNECTED
version:1.1
heart-beat:0,0
user-name:User{id=34, username='111',password='dab73e9a6c79ec96117d1474f68f2249'}
------------------------------------------------------
connected to server 
------------------------------------------------------
>>> SUBSCRIBE
id:sub-0
destination:/topic/helloEcho

连接成功信息,居然从服务器返回了当前登录用户的账号信息,但是为什么会把密码也搞过来啊 这个信息看起来很像我的User.toString() 还好我没把盐值也发出来 于是打了断点看了一下流程

1. 服务器收到 ws://localhost/hoody-websocket/ 连接后将会执行getName()方法 org.springframework.web.servlet.FrameworkServlet

protected String getUsernameForRequest(HttpServletRequest request) {
        Principal userPrincipal = request.getUserPrincipal();
        return (userPrincipal != null ? userPrincipal.getName() : null);
    }

2.如果你的用户类没有getName()方法 他就会!!! 获取 整个对象的toString() ShiroHttpServletRequest

 public String getName() {
       return getObject().toString();
  }

所以,检查你的User类,不要把密码也搞出去了,就算是加密的也不要

添加新评论,支持Markdown格式