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中
javaEventBus 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 访问
-
服务端:
javaRouter 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 格式的消息。
javaSockJSHandler 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,以过滤桥上面的信息,例子:javaRouter 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); }) );
修改原始消息:改变消息体、添加消息头
javasockJSHandler.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 认证处理器来处理登录和授权。例如:
javaPermittedOptions 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进行校验
-
服务端创建
javarouter.route().handler(CSRFHandler.create(vertx, "abracadabra"));
-
客户端携带"X-XSRF-TOKEN"请求头,值需要与服务端协商
HSTS处理器:
-
HTTP严格传输安全性(HSTS)是一种Web安全策略机制, 可帮助保护网站免受中间人攻击,例如协议降级攻击和cookie劫持。 它允许Web服务器要求Web浏览器应仅使用提供传输层安全性 (TLS/SSL)的HTTPS连接自动与其进行交互
javarouter.route().handler(HSTSHandler.create());
CSP处理器:
-
内容安全策略(CSP)是安全性的附加层,有助于检测和缓解某些类型的攻击, 包括跨站点脚本(XSS)和数据注入攻击。
CSP设计为完全向后兼容, 不支持CSP的浏览器只是忽略它,照常运行,默认为Web内容的标准同源策略。 如果该站点不提供CSP标头, 则浏览器同样会使用标准的同源策略。
javarouter.route().handler( CSPHandler.create() .addDirective("default-src", "*.trusted.com"));
XFrame处理器
-
X-Frame-Options
HTTP响应标头可用于指示是否应允许浏览器在frame
,iframe
,embed
或object
中呈现页面。 网站可以通过确保其内容未嵌入其他网站来避免点击劫持攻击。 -
仅当访问文档的用户使用支持
X-Frame-Options
的浏览器时, 才提供附加的安全性。 -
如果指定
DENY
,则从其他站点加载时,不仅在fram中加载页面失败,而且从同一站点加载时,也会失败。 另一方面,如果您指定SAMEORIGIN
,则只要frame中包含该页面的站点与提供该页面的站点相同, 您仍可以在frame中使用该页面。router.route().handler(XFrameHandler.create(XFrameHandler.DENY));