zoukankan      html  css  js  c++  java
  • springboot中通过stomp方式来处理websocket及token权限鉴权相关

    起因

    • 想处理后端向前端发送消息的情况,然后就了解到了原生websocketstomp协议方式来处理的几种方式,最终选择了stomp来,但很多参考资料都不全,导致费了很多时间,所以这里不说基础的内容了,只记录一些疑惑的点。

    相关前缀和注解

    在后台的websocket配置中,我们看到有/app/queue/topic/user这些前缀:

        @Override
        public void configureMessageBroker(MessageBrokerRegistry registry) {
            registry.enableSimpleBroker("/queue", "/topic");
            registry.setApplicationDestinationPrefixes("/app");//注意此处有设置
            registry.setUserDestinationPrefix("/user");
        }
    

    同时在controller中又有@MessageMapping@SubscribeMapping@SendTo@SendToUser等注解,这些前缀和这些注解是由一定的关系的,这边理一下:

    • 首先前端stompjs有两种方式向后端交互,一种是发送消息send,一种是订阅subscribe,它们在都会带一个目的地址/app/hello
    • 如果地址前缀是/app,那么此消息会关联到@MessageMapping(send命令会到这个注解)、@SubscribeMapping(subscribe命令会到这个注解)中,如果没有/app,则不会映射到任何注解上去,例如:
        //接收前端send命令发送的
        @MessageMapping("/hello")
        //@SendTo("/topic/hello2")
        public String hello(@Payload String message) {
            return "123";
        }
        //接收前端subscribe命令发送的
        @SubscribeMapping("/subscribe")
        public String subscribe() {
            return "456";
        }
        //接收前端send命令,但是单对单返回
        @MessageMapping("/test")
        @SendToUser("/queue/test")
        public String user(Principal principal, @Payload String message) {
            log.debug(principal.getName());
            log.debug(message);
            //可以手动发送,同样有queue
            //simpMessagingTemplate.convertAndSendToUser("admin","/queue/test","111");
            return "111";
        }
      
      当前端发送:send("/app/test",...)才会走到上方第一个中,而返回的这个123,并不是直接返回,而是默认将123转到/topic/hello这个订阅中去(自动在前面加上/topic),当然可以用@SendTo("/topic/hello2")中将123转到/topic/hello2这个订阅中;当前端发送subscribe("/app/subscribe",{接收直接返回的内容},会走到第二个中,而456就不经过转发了,直接会返回,当然也可以增加@SendTo("/topic/hello2")注解来不直接返回,而是转到其它订阅中。
    • 如果地址前缀是/topic,这个没什么说的,一般用于订阅消息,后台群发。
    • 如果地址前缀是/user,这个和一对一消息有关,而且会和queue有关联,前端必须同时增加queue,类似subscribe("/user/queue/test",...),后端的@SendToUser("/queue/test")同样要加queue才能正确的发送到前端订阅的地址。

    token鉴权相关

    权限相关一般是增加拦截器,网上查到的资料一般有两种方式:

    • 实现HandshakeInterceptor接口在beforeHandshake方法中来处理,这种方式缺点是无法获取header中的值,只能获取url中的参数,如果tokenjwt等很长的,用这种方式实现并不友好。
    • 实现ChannelInterceptor接口在preSend方法中来处理,这种方式可以获取header中的值,而且还可以设置用户信息等,详细见下方拦截器代码

    vue端相关注意点

    • vue端用websocket的好处是单页应用,不会频繁的断开和重连,所以相关代码放到App.vue
    • 由于要鉴权,所以需要登录后再连接,这里用的方法是watch监听token,如果token从无到有,说明刚登录,触发websocket连接。
    • 前端引入包npm instll sockjs-clientnpm install stompjs,具体代码见下方。

    相关代码

    • 后台配置
      @Configuration
      @EnableWebSocketMessageBroker
      @Slf4j
      public class WebsocketConfig implements WebSocketMessageBrokerConfigurer {
        @Autowired
        private AuthChannelInterceptor authChannelInterceptor;
      
        @Bean
        public WebSocketInterceptor getWebSocketInterceptor() {
            return new WebSocketInterceptor();
        }
      
        @Override
        public void registerStompEndpoints(StompEndpointRegistry registry) {
            registry.addEndpoint("/ws")//请求地址:http://ip:port/ws
                    .addInterceptors(getWebSocketInterceptor())//拦截器方式1,暂不用
                    .setAllowedOrigins("*")//跨域
                    .withSockJS();//开启socketJs
        }
      
        @Override
        public void configureMessageBroker(MessageBrokerRegistry registry) {
            registry.enableSimpleBroker("/queue", "/topic");
            registry.setApplicationDestinationPrefixes("/app");
            registry.setUserDestinationPrefix("/user");
        }
      
        /**
         * 拦截器方式2
         *
         * @param registration
         */
        @Override
        public void configureClientInboundChannel(ChannelRegistration registration) {
            registration.interceptors(authChannelInterceptor);
        }
      }
      
    • 拦截器
      @Component
      @Order(Ordered.HIGHEST_PRECEDENCE + 99)
      public class AuthChannelInterceptor implements ChannelInterceptor {
        /**
         * 连接前监听
         *
         * @param message
         * @param channel
         * @return
         */
        @Override
        public Message<?> preSend(Message<?> message, MessageChannel channel) {
            StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
            //1、判断是否首次连接
            if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
                //2、判断token
                List<String> nativeHeader = accessor.getNativeHeader("Authorization");
                if (nativeHeader != null && !nativeHeader.isEmpty()) {
                    String token = nativeHeader.get(0);
                    if (StringUtils.isNotBlank(token)) {
                        //todo,通过token获取用户信息,下方用loginUser来代替
                        if (loginUser != null) {
                            //如果存在用户信息,将用户名赋值,后期发送时,可以指定用户名即可发送到对应用户
                            Principal principal = new Principal() {
                                @Override
                                public String getName() {
                                    return loginUser.getUsername();
                                }
                            };
                            accessor.setUser(principal);
                            return message;
                        }
                    }
                }
                return null;
            }
            //不是首次连接,已经登陆成功
            return message;
        }
      }
      
    • 前端代码,放在App.vue中:
      import Stomp from 'stompjs'
      import SockJS from 'sockjs-client'
      import {mapGetters} from "vuex";
      
      export default {
        name: 'App',
        data() {
          return {
            stompClient: null,//由于不需要客户端给服务的发消息,所以暂不设置全局了
          }
        },
        computed: {
          ...mapGetters(["token"])
        },
        created() {
          //只有登录后才连接
          if (this.token) {
            this.initWebsocket();
          }
        },
        destroyed() {
          this.closeWebsocket()
        },
        watch: {
          token(val, oldVal) {
            //如果一开始没有,现在有了,说明刚登录,连接websocket
            if (!oldVal && val) {
              this.initWebsocket();
            }
            //如果原先由,现在没有了,说明退出登录,断开websocket
            if (oldVal && !val) {
              this.closeWebsocket();
            }
          }
        },
        methods: {
          initWebsocket() {
            let socket = new SockJS('http://localhost:8060/ws');
            this.stompClient = Stomp.over(socket);
            this.stompClient.connect(
              {"Authorization": this.token},//传递token
              (frame) => {
                //测试topic
                this.stompClient.subscribe("/topic/subscribe", (res) => {
                  console.log("订阅消息1:");
                  console.log(res);
                });
                //测试 @SubscribeMapping
                this.stompClient.subscribe("/app/subscribe", (res) => {
                  console.log("订阅消息2:");
                  console.log(res);
                });
                //测试单对单
                this.stompClient.subscribe("/user/queue/test", (res) => {
                  console.log("订阅消息3:");
                  console.log(res.body);
                });
                //测试发送
                this.stompClient.send("/app/test", {}, JSON.stringify({"user": "user"}))
              },
              (err) => {
                console.log("错误:");
                console.log(err);
                //10s后重新连接一次
                setTimeout(() => {
                  this.initWebsocket();
                }, 10000)
              }
            );
            this.stompClient.heartbeat.outgoing = 20000; //若使用STOMP 1.1 版本,默认开启了心跳检测机制(默认值都是10000ms)
            this.stompClient.heartbeat.incoming = 0; //客户端不从服务端接收心跳包
          },
          closeWebsocket() {
            if (this.stompClient !== null) {
              this.stompClient.disconnect(() => {
                console.log("关闭连接")
              })
            }
          }
        }
      }
      

    参考

  • 相关阅读:
    Jquery 总结的几种常用操作
    Mybatis 一对多
    HTML 子父窗口 iframe 超时 返回首页
    Struts 标签
    Spring + Mybatis 基于注解的事务
    机器学习实战-数据探索(变量变换、生成)
    机器学习实战-数据探索(变量变换、生成)
    Pandas matplotlib 无法显示中文 Ubuntu16.04
    Pandas matplotlib 无法显示中文 Ubuntu16.04
    Intel MKL FATAL ERROR: Cannot load libmkl_avx2.so or libmkl_def.so.
  • 原文地址:https://www.cnblogs.com/vishun/p/14334142.html
Copyright © 2011-2022 走看看