Vert.x学习(五)—— SockJS,搭建客户端,与后端服务器进行通信

SockJS

定义:
  • 为前端提供类似 WebSocket 的接口与后端的vertx服务器创建连接

SockJSHanlder

  • 用于在服务端接收客户端的请求

    java 复制代码
    //基本配置
    SockJSHandlerOptions options = new SockJSHandlerOptions()
            .setHeartbeatInterval(2000)
            //配置允许的客户端源
            .setOrigin("http://localhost:5173/");
    //创建SockJSHandler
    SockJSHandler sockJSHandler = SockJSHandler.create(vertx, options);
    //接收客户端请求
    sockJSHandler.socketHandler(sockJSSocket -> {
        sockJSSocket.handler(buffer -> {
            // 处理消息
            log.info("Received message: " + buffer.toString());
            // 将消息回写
            sockJSSocket.write(buffer);
        });
    });
    //将SockJSHandler注册到子路由当中,必须是子路由
    Router router = Router.router(vertx);
    router.route("/im/*")
      .subRouter(sockJSHandler.socketHandler(sockJSSocket -> {
        sockJSSocket.handler(buffer -> {
            // 处理消息
            log.info("Received message: " + buffer.toString());
            // 将消息回写
            sockJSSocket.write(buffer);
        });
    }));
    //服务端监听地址
    httpServer.requestHandler(router).listen(8088);

SockJS客户端

  • 在vue中使用SockJS示例:

    • 安装:npm install sockjs-client

    • 代码:

      vue 复制代码
      <script>
      	import SockJS from 'sockjs-client/dist/sockjs.min.js'
          //创建SockJS对象,与后端HTTP服务器监听的地址一致
          var sock = new SockJS('http://localhost:8088/im');
      
          sock.onopen = function () {
            sock.send('握个手');
            console.log('open');
          };
      
          sock.onmessage = function (e) {
            console.log('收到消息:', e.data);
          };
      
          sock.onevent = function (event, message) {
            console.log('event: %o, message:%o', event, message);
            return true; // 为了标记消息已被处理了
          };
      
          sock.onunhandled = function (json) {
            console.log('没有任何地址可以处理:', json);
          };
      
          sock.onclose = function () {
            console.log('close');
          };
      </script>

通过 event bus 交互 sockJSSocket

服务端:
  • 将sockJSSocket写入到event bus中

    java 复制代码
    EventBus eventBus = vertx.eventBus();
    Router router = Router.router(vertx);
    
    SockJSHandlerOptions options = new SockJSHandlerOptions()
      //开启注册事件处理器
      .setRegisterWriteHandler(true)
      //如果是集群,需要额外配置
      .setLocalWriteHandler(false);
    
    SockJSHandler sockJSHandler = SockJSHandler.create(vertx, options);
    
    router.route("/myapp/*")
      .subRouter(sockJSHandler.socketHandler(sockJSSocket -> {
    
      // 获取 writeHandlerID 并将其存放 (例如放在LocalMap中)
      String writeHandlerID = sockJSSocket.writeHandlerID();
      eventBus.send(writeHandlerID, Buffer.buffer("foo"));
    }));
    //服务端监听地址
    httpServer.requestHandler(router).listen(8088);
SockJS 桥接 Event Bus:
  • 作用:使得 event bus 不仅可以在服务器端多个 Vert.x 实例中使用,还可以通过运行在浏览器里的 JavaScript 访问

  • 服务端:

    java 复制代码
    Router router = Router.router(vertx);
    
    SockJSHandler sockJSHandler = SockJSHandler.create(vertx);
    // 连接桥配置
    SockJSBridgeOptions options = new SockJSBridgeOptions();
    // 将连接桥挂载到子路由上
    router
      .route("/eventbus/*")
      .subRouter(sockJSHandler.bridge(options));
    //服务端监听地址
    httpServer.requestHandler(router).listen(8088);
  • 客户端:

    安装:npm install @vertx/eventbus-bridge-client.js

    代码:

    vue 复制代码
    <template>
      <div id="app">
        <button @click="im">发送消息</button>
      </div>
    </template>
    
    <script>
    import EventBus from '@vertx/eventbus-bridge-client.js/vertx-eventbus.js'
    export default {
      name: 'App',
      data () {
        return {
          eb: null
        }
      },
      mounted () {
        this.eb = new EventBus('http://localhost:8080/eventbus');
        this.eb.onopen = () => {
          console.log("open")
          // 设置一个处理器以接收消息
          this.eb.registerHandler('client1', (error, message) => {
            console.log('收到消息: ' + JSON.stringify(message));
          });
        }
        this.eb.onunhandled = (json) => {
          console.warn(json)
        }
        this.eb.onerror = (err) => {
          console.error(err)
        }
      },
    
      methods:{
        im: function(){
          this.eb.send('server1', {name: 'ailu', age: 666});
        }
      }
    }
    
    </script>
  • 守护连接桥

    按上面的例子此时还无法接收消息,因为为避免event bus将信息发送到任意客户端和服务端,所以SockJS连接桥默认会拒绝所有消息。因此客户端和服务端之间需要建立匹配规则来守护连接桥,即通过SockJSBridgeOptions配置。

    每一个匹配规则 对应一个 PermittedOptions 对象:

    • setAddress:定义消息可以被发送到哪些地址。
    • setAddressRegex:通过正则表达式来定义消息可以被发送到哪些地址。
    • setMatch:通过消息的结构 来控制消息是否可被发送 。该配置中定义的每一个字段必须在消息中存在,且值一致。 目前仅适用于 JSON 格式的消息。
    java 复制代码
    SockJSHandler sockJSHandler = SockJSHandler.create(vertx);
    
    // 客户端 -> 服务端 的匹配规则
    
    // 允许客户端向地址 `server1` 发送消息
    PermittedOptions inboundPermitted1 = new PermittedOptions()
      .setAddress("server1");
    
    // 允许客户端向地址 `server2` 发送,且消息必须包含有值为ailu的name字段
    PermittedOptions inboundPermitted2 = new PermittedOptions()
      .setAddress("server2")
      .setMatch(new JsonObject().put("name", "ailu"));
    
                
    // 服务端 -> 客户端 的匹配规则
                
    // 允许服务端向客户端地址为 `client1` 的消息
    PermittedOptions outboundPermitted1 = new PermittedOptions()
      .setAddress("client1");
    
    // 允许服务端向客户端地址为 `client2.` 开头的消息
    PermittedOptions outboundPermitted2 = new PermittedOptions()
      .setAddressRegex("client2\\..+");
    
    //将匹配规则注册到SockJSBridgeOptions中
    SockJSBridgeOptions options = new SockJSBridgeOptions().
      addInboundPermitted(inboundPermitted1).
      addInboundPermitted(inboundPermitted2).
      addOutboundPermitted(outboundPermitted1).
      addOutboundPermitted(outboundPermitted2);
    
    // 将连接桥挂载到路由器上
    router
      .route("/eventbus/*")
      .subRouter(sockJSHandler.bridge(options));
  • 处理事件总线桥事件:希望在桥时上发生事件时(BridgeEvent 实例进)收到通知,可以提供一个处理器在调用 bridge 时调用,事件类型有:

    • SOCKET_CREATED:创建新的 SockJS 套接字时,将发生此事件.
    • SOCKET_IDLE:当 SockJS 套接字闲置的时间比最初配置的时间长时,将发生此事件
    • SOCKET_PING:当为 SockJS 套接字更新最后的 ping 时间戳时,将发生此事件.
    • SOCKET_CLOSED:关闭 SockJS 套接字时,将发生此事件.
    • SOCKET_ERROR:当底层传输出错时,将发生此事件.
    • SEND:尝试消息从客户端发送到服务器时,将发生此事件.
    • PUBLISH:尝试消息从客户端发布到服务器时,将发生此事件.
    • RECEIVE:尝试消息从服务器传递到客户端时,将发生此事件.
    • REGISTER:当客户端尝试注册处理程序时,将发生此事件.
    • UNREGISTER:当客户端尝试注销处理程序时,将发生此事件

    可以通过BridgeEvent.getRawMessage获取事件的原始信息,获取内容如下:

    java 复制代码
    //正常事件存储的数据
    {
     "type": "send"|"publish"|"receive"|"register"|"unregister",
     "address": 发送/发布/注册/注销的信息总线地址
     "body": 消息体
    }
    
    //SOCKET_ERROR时存储的数据
    {
     "type": "err",
     "failureType": "socketException",
     "message": "可选的,来自被引发的异常的消息"
    }

    事件(BridgeEvent)是Promise的一个实例,处理完事件后可以用true来完成promise以进一步处理。如果不希望处理事件,则可以用false来完成promise,以过滤桥上面的信息,例子:

    java 复制代码
    Router router = Router.router(vertx);
    
    // 让客户端发送到 "server"的任何消息通过
    PermittedOptions inboundPermitted = new PermittedOptions()
      .setAddress("server");
    
    SockJSHandler sockJSHandler = SockJSHandler.create(vertx);
    SockJSBridgeOptions options = new SockJSBridgeOptions()
      .addInboundPermitted(inboundPermitted);
    
    // 将桥挂载到子路由器上
    router
        .route("/eventbus/*")
        .subRouter(sockJSHandler
                .bridge(options, bridgeEvent -> {
                    //如果发布类型为 SEND 或 PUBLISH,表示客户端发送消息
                    if(bridgeEvent.type() == BridgeEventType.SEND
                    || bridgeEvent.type() == BridgeEventType.PUBLISH){
                        log.info("Bridge event: " + bridgeEvent.type());
                        log.info("收到消息:" + bridgeEvent.getRawMessage());
                        //获取socket对象
                        SockJSSocket socket = bridgeEvent.socket();
                        //消息传递给客户端
                        ChatMessge msg = ChatMessage.builder().content("AI").build();
                		socket.write(Json.encode(msg));
                    }
                    // 注意不要忘了complete事件!
                    bridgeEvent.complete(true);
                })
        );

    修改原始消息:改变消息体、添加消息头

    java 复制代码
    sockJSHandler.bridge(options, be -> {
        if (
          be.type() == BridgeEventType.PUBLISH ||
            be.type() == BridgeEventType.SEND) {
    
          // 添加一些头
          JsonObject headers = new JsonObject()
            .put("header1", "val")
            .put("header2", "val2");
    
            JsonObject rawMessage = be.getRawMessage();
            rawMessage.put("headers", headers);
            be.setRawMessage(rawMessage);
          }
          be.complete(true);
        })

    也可以直接通过Event Bus来交互消息

    java 复制代码
    //向clinet1地址发送消息
    vertx.eventBus().send("client1",Json.encode(ChatMessage.builder().content("ailu").build()));
  • 消息授权

    可以通过setMatch方法进行权限控制

    或者通过 setRequiredAuthority 方法来指定对于登录用户,需要具有哪些权限才允许访问这个消息,此时需要配置一个 Vert.x 认证处理器来处理登录和授权。

    例如:

    java 复制代码
    PermittedOptions inboundPermitted = new PermittedOptions()
      .setAddress("demo");
      // 仅限用户已登录并且拥有权限 `write`
      .setRequiredAuthority("write");
    //配置匹配规则
    SockJSBridgeOptions options = new SockJSBridgeOptions()
      .addInboundPermitted(inboundPermitted);
    SockJSHandler sockJSHandler = SockJSHandler.create(vertx);
    
    //创建Session存储器、注册Session处理器
    router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx)));
    // 设置基础认证处理器
    AuthenticationHandler basicAuthHandler = BasicAuthHandler.create(authProvider);
    
    router.route("/eventbus/*").handler(basicAuthHandler);
    // 将连接桥挂载到路由器上
    router
      .route("/eventbus/*")
      .subRouter(sockJSHandler.bridge(options));

跨站请求伪造:

  • 解决:使用唯一token进行校验

  • 服务端创建

    java 复制代码
    router.route().handler(CSRFHandler.create(vertx, "abracadabra"));
  • 客户端携带"X-XSRF-TOKEN"请求头,值需要与服务端协商

HSTS处理器:

  • HTTP严格传输安全性(HSTS)是一种Web安全策略机制, 可帮助保护网站免受中间人攻击,例如协议降级攻击和cookie劫持。 它允许Web服务器要求Web浏览器应仅使用提供传输层安全性 (TLS/SSL)的HTTPS连接自动与其进行交互

    java 复制代码
    router.route().handler(HSTSHandler.create());

CSP处理器:

  • 内容安全策略(CSP)是安全性的附加层,有助于检测和缓解某些类型的攻击, 包括跨站点脚本(XSS)和数据注入攻击。

    CSP设计为完全向后兼容, 不支持CSP的浏览器只是忽略它,照常运行,默认为Web内容的标准同源策略。 如果该站点不提供CSP标头, 则浏览器同样会使用标准的同源策略。

    java 复制代码
    router.route().handler(
      CSPHandler.create()
        .addDirective("default-src", "*.trusted.com"));

XFrame处理器

  • X-Frame-Options HTTP响应标头可用于指示是否应允许浏览器在 frameiframeembedobject 中呈现页面。 网站可以通过确保其内容未嵌入其他网站来避免点击劫持攻击。

  • 仅当访问文档的用户使用支持 X-Frame-Options 的浏览器时, 才提供附加的安全性。

  • 如果指定 DENY,则从其他站点加载时,不仅在fram中加载页面失败,而且从同一站点加载时,也会失败。 另一方面,如果您指定 SAMEORIGIN ,则只要frame中包含该页面的站点与提供该页面的站点相同, 您仍可以在frame中使用该页面。

    router.route().handler(XFrameHandler.create(XFrameHandler.DENY));

相关推荐
GalenWu几秒前
对象转换为 JSON 字符串(或反向解析)
前端·javascript·微信小程序·json
Kookoos10 分钟前
ABP vNext + EF Core 实战性能调优指南
数据库·后端·c#·.net·.netcore
GUIQU.10 分钟前
【Vue】微前端架构与Vue(qiankun、Micro-App)
前端·vue.js·架构
数据潜水员17 分钟前
插槽、生命周期
前端·javascript·vue.js
zm23 分钟前
服务器多客户端连接核心要点(1)
java·开发语言
2401_8370885023 分钟前
CSS vertical-align
前端·html
优雅永不过时·29 分钟前
实现一个漂亮的Three.js 扫光地面 圆形贴图扫光
前端·javascript·智慧城市·three.js·贴图·shader
FuckPatience32 分钟前
关于C#项目中 服务层使用接口的问题
java·开发语言·c#
LouSean39 分钟前
Unity按钮事件冒泡
经验分享·笔记·学习·unity·游戏引擎
天上掉下来个程小白1 小时前
缓存套餐-01.Spring Cache介绍和常用注解
java·redis·spring·缓存·spring cache·苍穹外卖