! 版权声明: 本博客内容均均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。
目录[-]
如果对你有帮助,别忘了 Github 点颗星嘞~
WebSocket 是一种基于 TCP 的网络协议。在 2009 年诞生,于 2011 年被 IETF 定为标准 RFC 6455 通信标准,并由 RFC7936 补充规范。WebSocket API 也被 W3C 定为标准。
WebSocket 也是一种全双工通信的协议,既允许客户端向服务器主动发送消息,也允许服务器主动向客户端发送消息。在 WebSocket 中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,进行双向数据传输。
谈起为什么需要 WebSocket 前,那得先了解在没有 WebSocket 那段时间说起,那时候基于 Web 的消息基本上是靠 Http 协议进行通信,而经常有”聊天室”、”消息推送”、”股票信 息实时动态”等这样需求,而实现这样的需求常用的有以下几种解决方案:
短轮询是指客户端每隔一段时间就询问一次服务器是否有新的消息,如果有就接收消息。这样方式会增加很多次无意义的发送请求信息,每次都会耗费流量及处理器资源。
——————
长轮询是段轮询的改进,客户端执行 HTTP 请求发送消息到服务器后,等待服务器回应,如果没有新的消息就一直等待,知道服务器有新消息传回或者超时。这也是个反复的过程,这种做法只是减小了网络带宽和处理器的消耗,但是带来的问题是导致消息实时性低,延迟严重。而且也是基于循环,最根本的带宽及处理器资源占用并没有得到有效的解决。
——————
目前除了 IE/Edge,其他浏览器都支持。
服务器发送事件是一种服务器向浏览器客户端发起数据传输的技术。一旦创建了初始连接,事件流将保持打开状态,直到客户端关闭。该技术通过传统的 HTTP 发送,并具有 WebSockets 缺乏的各种功能,例如”自动重新连接”、”事件ID” 及 “发送任意事件”的能力。
服务器发送事件是单向通道,只能服务器向浏览器发送,因为流信息本质上就是下载。
——————
显然,上面这几种方式都有各自的优缺点,虽然靠轮询方式能够实现这些一些功能,但是其对性能的开销和低效率是非常致命的,尤其是在移动端流行的现在。现在客户端与服务端双向通信的需求越来越多,且现在的浏览器大部分都支持 WebSocket。所以对实时性和双向通信及其效率有要求的话,比较推荐使用 WebSocket。
客户端 HTTP 请求的 Header 头信息如下:
Connection: Upgrade Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits Sec-WebSocket-Key: IRQYhWINfX5Fh1zdocDl6Q== Sec-WebSocket-Version: 13 Upgrade: websocket
服务端响应的 HTTP Header 头信息如下:
Connection: upgrade Sec-Websocket-Accept: TSF8/KitM+yYRbXmjclgl7DwbHk= Upgrade: websocket
WebSocket 确实指定了一种消息传递体系结构,但并不强制使用任何特定的消息传递协议。而且它是 TCP 上的一个非常薄的层,它将字节流转换为消息流(文本或二进制)仅此而已。由应用程序来解释消息的含义。
与 HTTP(它是应用程序级协议)不同,在 WebSocket 协议中, 传入消息中根本没有足够的信息供框架或容器知道如何路由或处理它 。因此,对于非常琐碎的应用程序而言 WebSocket 协议的级别可以说太低了。 可以做到的是引导在其上面再创建一层框架。这就相当于当今大多数 Web 应用程序使用的是 Web 框架,而不直接仅使用 Servlet API 进行编码一样 。
WebSocket RFC 定义了子协议的使用。在握手过程中,客户机和服务器可以使用头 Sec-WebSocket 协议商定子协议,即使不需要使用子协议,而是用更高的应用程序级协议,但应用程序仍需要选择客户端和服务器都可以理解的消息格式。且该格式可以是自定义的、特定于框架的或标准的消息传递协议。
Spring 框架支持使用 STOMP,这是一个简单的消息传递协议,最初创建用于脚本语言,框架灵感来自 HTTP。STOMP 被广泛支持,非常适合在 WebSocket 和 web 上使用。
关于详细的 STOMP 请查看 STOMP 协议规范 了解。
STOMP(Simple Text-Orientated Messaging Protocol)是一种简单的面向文本的消息传递协议。它提供了一个可互操作的连接格式,允许 STOMP 客户端与任意 STOMP 消息代理(Broker)进行交互。STOMP 协议由于设计简单,易于开发客户端,因此在多种语言和多种平台上得到广泛地应用。
STOMP 是基于帧的协议,其帧以 HTTP 为模型。STOMP 结构为:
COMMAND header1:value1 header2:value2 Body^@
客户端可以使用 SEND 或 SUBSCRIBE 命令发送或订阅消息,还可以使用 “destination” 头来描述消息的内容和接收者。这支持一种简单的发布-订阅机制,可用于通过代理将消息发送到其他连接的客户端,或将消息发送到服务器以请求执行某些工作。
STOMP 的客户端和服务器之间的通信是通过” 帧 “(Frame)实现的,每个帧由多” 行 “(Line)组成,其包含的帧如下:
直接使用 WebSocket 就很类似于使用 TCP 套接字来编写 Web 应用,因为没有高层级的应用协议(wire protocol),因而就需要我们定义应用之间所发送消息的语义,还需要确保连接的两端都能遵循这些语义。同 HTTP 在 TCP 套接字上添加请求-响应模型层一样,STOMP 在 WebSocket 之上提供了一个基于帧的线路格式层,用来定义消息语义。
使用 Spring 的 STOMP 支持时,Spring WebSocket 应用程序充当客户端的 STOMP 代理。消息被路由到 @Controller 消息处理方法或简单的内存中代理,该代理跟踪订阅并向订阅的用户广播消息。还可以将 Spring 配置为与专用的 STOMP 代理(例如 RabbitMQ,ActiveM Q等)一起使用,以实际广播消息。在那种情况下,Spring 维护与代理的 TCP 连接,将消息中继到该代理,并将消息从该代理向下传递到已连接的 WebSocket 客户端。因此 Spring Web 应用程序可以依赖基于统一 HTTP 的安全性,通用验证以及熟悉的编程模型消息处理工作。
Spring 官方提供的处理流图:
上面中的一些概念关键词:
上面的设置包括3个消息通道:
WebSocket 常分为广播与队列模式,广播模式是向订阅广播的用户发送信息,只要订阅相关广播就能收到对应信息。而队列模式常用于点对点模式,为单个用户向另一个用户发送信息,这里先介绍下广播模式的实现示例。
这里使用 Maven 工具管理依赖包,分别引入下面依赖:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.1.RELEASE</version> </parent> <groupId>mydlq.club</groupId> <artifactId>springboot-websocket-topic-example</artifactId> <version>0.0.1</version> <name>springboot-websocket-topic-example</name> <description>Spring Boot WebSocket Example</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!-- SpringBoot WebSocket --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
创建便于传输消息的实体类,里面字段内容如下:
import lombok.Data; @Data public class MessageBody { /** 消息内容 */ private String content; /** 广播转发的目标地址(告知 STOMP 代理转发到哪个地方) */ private String destination; }
创建 WebSocket 配置类,配置进行连接注册的端点 /mydlq
和消息代理前缀 /topic
及接收客户端发送消息的前缀 /app
。
import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { /** * 配置 WebSocket 进入点,及开启使用 SockJS,这些配置主要用配置连接端点,用于 WebSocket 连接 * * @param registry STOMP 端点 */ @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/mydlq").withSockJS(); } /** * 配置消息代理选项 * * @param registry 消息代理注册配置 */ @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 设置一个或者多个代理前缀,在 Controller 类中的方法里面发生的消息,会首先转发到代理从而发送到对应广播或者队列中。 registry.enableSimpleBroker("/topic"); // 配置客户端发送请求消息的一个或多个前缀,该前缀会筛选消息目标转发到 Controller 类中注解对应的方法里 registry.setApplicationDestinationPrefixes("/app"); } }
创建 Controller 类,该类也类似于正常 Web 项目中 Controller 写法一样,在方法上面添加 @MessageMapping
注解,当客户端发送消息请求的前缀匹配上 WebSocket
配置类中的 /app
前缀后,会进入到 Controller
类中进行匹配,如果匹配成功则执行注解所在的方法内容。
import mydlq.club.model.MessageBody; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.SimpMessageSendingOperations; import org.springframework.stereotype.Controller; @Controller public class MessageController { /** 消息发送工具对象 */ @Autowired private SimpMessageSendingOperations simpMessageSendingOperations; /** 广播发送消息,将消息发送到指定的目标地址 */ @MessageMapping("/test") public void sendTopicMessage(MessageBody messageBody) { // 将消息发送到 WebSocket 配置类中配置的代理中(/topic)进行消息转发 simpMessageSendingOperations.convertAndSend(messageBody.getDestination(), messageBody); } }
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
创建用于操作 WebSocket 的 JS 文件 app-websocket.js
,内容如下:
// 设置 STOMP 客户端 var stompClient = null; // 设置 WebSocket 进入端点 var SOCKET_ENDPOINT = "/mydlq"; // 设置订阅消息的请求前缀 var SUBSCRIBE_PREFIX = "/topic" // 设置订阅消息的请求地址 var SUBSCRIBE = ""; // 设置服务器端点,访问服务器中哪个接口 var SEND_ENDPOINT = "/app/test"; /* 进行连接 */ function connect() { // 设置 SOCKET var socket = new SockJS(SOCKET_ENDPOINT); // 配置 STOMP 客户端 stompClient = Stomp.over(socket); // STOMP 客户端连接 stompClient.connect({}, function (frame) { alert("连接成功"); }); } /* 订阅信息 */ function subscribeSocket(){ // 设置订阅地址 SUBSCRIBE = SUBSCRIBE_PREFIX + $("#subscribe").val(); // 输出订阅地址 alert("设置订阅地址为:" + SUBSCRIBE); // 执行订阅消息 stompClient.subscribe(SUBSCRIBE, function (responseBody) { var receiveMessage = JSON.parse(responseBody.body); $("#information").append("<tr><td>" + receiveMessage.content + "</td></tr>"); }); } /* 断开连接 */ function disconnect() { stompClient.disconnect(function() { alert("断开连接"); }); } /* 发送消息并指定目标地址(这里设置的目标地址为自身订阅消息的地址,当然也可以设置为其它地址) */ function sendMessageNoParameter() { // 设置发送的内容 var sendContent = $("#content").val(); // 设置待发送的消息内容 var message = '{"destination": "' + SUBSCRIBE + '", "content": "' + sendContent + '"}'; // 发送消息 stompClient.send(SEND_ENDPOINT, {}, message); }
创建用于展示 WebSocket 相关功能的 WEB HTML 页面 index.html
,内容如下:
<!DOCTYPE html> <html> <head> <title>Hello WebSocket</title> <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/sockjs-client/1.4.0/sockjs.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script> <script src="app-websocket.js"></script> </head> <body> <div id="main-content" class="container" style="margin-top: 10px;"> <div class="row"> <form class="navbar-form" style="margin-left:0px"> <div class="col-md-12"> <div class="form-group"> <label>WebSocket 连接:</label> <button class="btn btn-primary" type="button" onclick="connect();">进行连接</button> <button class="btn btn-danger" type="button" onclick="disconnect();">断开连接</button> </div> <label>订阅地址:</label> <div class="form-group"> <input type="text" id="subscribe" class="form-control" placeholder="订阅地址"> </div> <button class="btn btn-warning" onclick="subscribeSocket();" type="button">订阅</button> </div> </form> </div> </br> <div class="row"> <div class="form-group"> <label for="content">发送的消息内容:</label> <input type="text" id="content" class="form-control" placeholder="消息内容"> </div> <button class="btn btn-info" onclick="sendMessageNoParameter();" type="button">发送</button> </div> </br> <div class="row"> <div class="col-md-12"> <h5 class="page-header" style="font-weight:bold">接收到的消息:</h5> <table class="table table-striped"> <tbody id="information"></tbody> </table> </div> </div> </div> </body> </html>
输入地址 http://localhost:8080/index.html 访问测试的前端页面,然后执行下面步骤进行测试:
执行完上面步骤成后,可以观察到成功接收到订阅地址的消息,如下:
这里使用 Maven 工具管理依赖包,分别引入下面依赖:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.1.RELEASE</version> </parent> <groupId>mydlq.club</groupId> <artifactId>springboot-websocket-queue-security-example</artifactId> <version>0.0.1</version> <name>springboot-websocket-queue-security-example</name> <description>Spring Boot WebSocket Example</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!-- SpringBoot WebSocket --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <!-- Security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
创建便于传输消息的实体类,里面字段内容如下:
import lombok.Data; @Data public class MessageBody { /** 发送消息的用户 */ private String from; /** 消息内容 */ private String content; /** 目标用户(告知 STOMP 代理转发到哪个用户) */ private String targetUser; /** 广播转发的目标地址(告知 STOMP 代理转发到哪个地方) */ private String destination; }
创建 WebSocket 配置类,配置进行连接注册的端点 /mydlq
和消息代理前缀 /queue
及接收客户端发送消息的前缀 /app
。
import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { /** * 配置 WebSocket 进入点,及开启使用 SockJS,这些配置主要用配置连接端点,用于 WebSocket 连接 * * @param registry STOMP 端点 */ @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/mydlq").withSockJS(); } /** * 配置消息代理选项 * * @param registry 消息代理注册配置 */ @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 设置一个或者多个代理前缀,在 Controller 类中的方法里面发生的消息,会首先转发到代理从而发送到对应广播或者队列中。 registry.enableSimpleBroker("/queue"); // 配置客户端发送请求消息的一个或多个前缀,该前缀会筛选消息目标转发到 Controller 类中注解对应的方法里 registry.setApplicationDestinationPrefixes("/app"); // 服务端通知特定用户客户端的前缀,可以不设置,默认为user registry.setUserDestinationPrefix("/user"); } }
Spring Security 的配置类,可以在该类中配置权限认证及测试的两个用户相关信息:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 设置密码编码的配置参数,这里设置为 NoOpPasswordEncoder,不配置密码加密,方便测试。 * * @return 密码编码实例 */ @Bean PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } /** * 设置权限认证参数,这里用于创建两个用于测试的用户信息。 * * @param auth SecurityBuilder 用于创建 AuthenticationManager。 * @throws Exception 抛出的异常 */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("mydlq1") .password("123456") .roles("admin") .and() .withUser("mydlq2") .password("123456") .roles("admin"); } /** * 设置 HTTP 安全相关配置参数 * * @param http HTTP Security 对象 * @throws Exception 抛出的异常信息 */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and() .formLogin() .permitAll(); } }
跟上面介绍广播模式一样,作用也是根据 WebSocket 配置类中 /app 前缀匹配后进入 Controller 类进行逻辑处理操作。
import mydlq.club.example.model.MessageBody; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.SimpMessageSendingOperations; import org.springframework.stereotype.Controller; import java.security.Principal; @Controller public class MessageController { @Autowired private SimpMessageSendingOperations simpMessageSendingOperations; /** * 点对点发送消息,将消息发送到指定用户 */ @MessageMapping("/test") public void sendUserMessage(Principal principal, MessageBody messageBody) { // 设置发送消息的用户 messageBody.setFrom(principal.getName()); // 调用 STOMP 代理进行消息转发 simpMessageSendingOperations.convertAndSendToUser(messageBody.getTargetUser(), messageBody.getDestination(), messageBody); } }
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
创建用于操作 WebSocket 的 JS 文件 app-websocket.js,内容如下:
// 设置 STOMP 客户端 var stompClient = null; // 设置 WebSocket 进入端点 var SOCKET_ENDPOINT = "/mydlq"; // 设置订阅消息的请求前缀 var SUBSCRIBE_PREFIX = "/topic" // 设置订阅消息的请求地址 var SUBSCRIBE = ""; // 设置服务器端点,访问服务器中哪个接口 var SEND_ENDPOINT = "/app/test"; /* 进行连接 */ function connect() { // 设置 SOCKET var socket = new SockJS(SOCKET_ENDPOINT); // 配置 STOMP 客户端 stompClient = Stomp.over(socket); // STOMP 客户端连接 stompClient.connect({}, function (frame) { alert("连接成功"); }); } /* 订阅信息 */ function subscribeSocket(){ // 设置订阅地址 SUBSCRIBE = SUBSCRIBE_PREFIX + $("#subscribe").val(); // 输出订阅地址 alert("设置订阅地址为:" + SUBSCRIBE); // 执行订阅消息 stompClient.subscribe(SUBSCRIBE, function (responseBody) { var receiveMessage = JSON.parse(responseBody.body); $("#information").append("<tr><td>" + receiveMessage.content + "</td></tr>"); }); } /* 断开连接 */ function disconnect() { stompClient.disconnect(function() { alert("断开连接"); }); } /* 发送消息并指定目标地址 */ function sendMessageNoParameter() { // 设置发送的内容 var sendContent = $("#content").val(); // 设置待发送的消息内容 var message = '{"destination": "' + SUBSCRIBE + '", "content": "' + sendContent + '"}'; // 发送消息 stompClient.send(SEND_ENDPOINT, {}, message); }
创建用于展示 WebSocket 相关功能的 WEB HTML 页面 index.html,内容如下:
<!DOCTYPE html> <html> <head> <title>Hello WebSocket</title> <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/sockjs-client/1.4.0/sockjs.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script> <script src="app-websocket.js"></script> </head> <body> <div id="main-content" class="container" style="margin-top: 10px;"> <div class="row"> <form class="navbar-form" style="margin-left:0px"> <div class="col-md-12"> <div class="form-group"> <label>WebSocket 连接:</label> <button class="btn btn-primary" type="button" onclick="connect();">进行连接</button> <button class="btn btn-danger" type="button" onclick="disconnect();">断开连接</button> </div> <label>订阅地址:</label> <div class="form-group"> <input type="text" id="subscribe" class="form-control" placeholder="订阅地址"> </div> <button class="btn btn-warning" onclick="subscribeSocket();" type="button">订阅</button> </div> </form> </div> </br> <div class="row"> <div class="form-group"> <label for="content">发送的消息内容:</label> <input type="text" id="content" class="form-control" placeholder="消息内容"> </div> <button class="btn btn-info" onclick="sendMessageNoParameter();" type="button">发送</button> </div> </br> <div class="row"> <div class="col-md-12"> <h5 class="page-header" style="font-weight:bold">接收到的消息:</h5> <table class="table table-striped"> <tbody id="information"></tbody> </table> </div> </div> </div> </body> </html>
为了方便测试,需要打开两个不同类型浏览器(因为用户登录后会存 Session,如果一个浏览器不同用户登录会使之前 Session 失效)来进行测试,两个浏览器同时输入地址 http://localhost:8080/index.html 访问测试的前端页面,然后可以看到并没有进入 /index.html
页面,而是跳转到 Spring Security 提供的登录的 /login
页面,如下:
两个浏览器中都输入用户名/密码 mydlq1/123456
与 mydlq2/123456
进行登录,然后会回到 /index.html
页面,然后执行下面步骤进行测试:
执行完上面步骤成后,可以在两个不同浏览器中观察到如下内容:
这里使用 Maven 工具管理依赖包,分别引入下面依赖:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.1.RELEASE</version> </parent> <groupId>mydlq.club</groupId> <artifactId>springboot-websocket-queue-security-custom-example</artifactId> <version>0.0.1</version> <name>springboot-websocket-queue-security-custom-example</name> <description>Spring Boot WebSocket Example</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!-- SpringBoot WebSocket --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
import lombok.Data; @Data public class MessageBody { /** 发送消息的用户 */ private String from; /** 消息内容 */ private String content; /** 目标用户(告知 STOMP 代理转发到哪个用户) */ private String targetUser; /** 广播转发的目标地址(告知 STOMP 代理转发到哪个地方) */ private String destination; }
import lombok.AllArgsConstructor; import lombok.Data; @Data @AllArgsConstructor public class User { private String username; private String token; }
配置 WebSocket 通道拦截器,里面添加两个模拟用户:
import mydlq.club.model.User; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.simp.stomp.StompCommand; import org.springframework.messaging.simp.stomp.StompHeaderAccessor; import org.springframework.messaging.support.ChannelInterceptor; import org.springframework.messaging.support.MessageHeaderAccessor; import java.security.Principal; import java.util.List; import java.util.Map; /** * WebSocket 通道拦截器(这里模拟两个测试 Token 方便测试,不做具体 Token 鉴权实现) * * @author mydlq */ public class MyChannelInterceptor implements ChannelInterceptor { /** 测试用户与 token 1 */ private User mydlq1 = new User("","123456-1"); /** 测试用户与 token 2 */ private User mydlq2 = new User("","123456-2"); /** * 从 Header 中获取 Token 进行验证,根据不同的 Token 区别用户 * * @param message 消息对象 * @param channel 通道对象 * @return 验证后的用户信息 */ @Override public Message<?> preSend(Message<?> message, MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); String token = getToken(message); if (token!=null && accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) { Principal user = null; // 提前创建好两个测试 token 进行匹配,方便测试 if (mydlq1.getToken().equals(token)){ user = () -> mydlq1.getUsername(); } else if (mydlq2.getToken().equals(token)){ user = () -> mydlq2.getUsername(); } accessor.setUser(user); } return message; } /** * 从 Header 中获取 TOKEN * * @param message 消息对象 * @return TOKEN */ private String getToken(Message<?> message){ Map<String,Object> headers = (Map<String, Object>) message.getHeaders().get("nativeHeaders"); if (headers !=null && headers.containsKey("token")){ List<String> token = (List<String>)headers.get("token"); return String.valueOf(token.get(0)); } return null; } }
创建 WebSocket 配置类,配置进行连接注册的端点 /mydlq
和消息代理前缀 /queue
及接收客户端发送消息的前缀 /app
。
import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.ChannelRegistration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { /** * 配置 WebSocket 进入点,及开启使用 SockJS,这些配置主要用配置连接端点,用于 WebSocket 连接 * * @param registry STOMP 端点 */ @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/mydlq").withSockJS(); } /** * 配置消息代理选项 * * @param registry 消息代理注册配置 */ @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 设置一个或者多个代理前缀,在 Controller 类中的方法里面发生的消息,会首先转发到代理从而发送到对应广播或者队列中。 registry.enableSimpleBroker("/queue"); // 配置客户端发送请求消息的一个或多个前缀,该前缀会筛选消息目标转发到 Controller 类中注解对应的方法里 registry.setApplicationDestinationPrefixes("/app"); // 服务端通知特定用户客户端的前缀,可以不设置,默认为user registry.setUserDestinationPrefix("/user"); } /** * 配置通道拦截器,用于获取 Header 的 Token 进行鉴权 * * @param registration 注册通道配置类 */ @Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors(new MyChannelInterceptor()); } }
import mydlq.club.example.model.MessageBody; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.SimpMessageSendingOperations; import org.springframework.stereotype.Controller; import java.security.Principal; @Controller public class MessageController { @Autowired private SimpMessageSendingOperations simpMessageSendingOperations; /** * 点对点发送消息,将消息发送到指定用户 */ @MessageMapping("/test") public void sendUserMessage(Principal principal, MessageBody messageBody) { // 设置发送消息的用户 messageBody.setFrom(principal.getName()); // 调用 STOMP 代理进行消息转发 simpMessageSendingOperations.convertAndSendToUser(messageBody.getTargetUser(), messageBody.getDestination(), messageBody); } }
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
创建用于操作 WebSocket 的 JS 文件 app-websocket.js,内容如下:
// 设置 STOMP 客户端 var stompClient = null; // 设置 WebSocket 进入端点 var SOCKET_ENDPOINT = "/mydlq"; // 设置订阅消息的请求地址前缀 var SUBSCRIBE_PREFIX = "/queue"; // 设置订阅地址 var SUBSCRIBE = ""; // 设置服务器端点,访问服务器中哪个接口 var SEND_ENDPOINT = "/app/test"; /* 进行连接 */ function connect() { // 设置 SOCKET var socket = new SockJS(SOCKET_ENDPOINT); // 配置 STOMP 客户端 stompClient = Stomp.over(socket); // 获取 TOKEN var myToken = $("#myToken").val(); // STOMP 客户端连接 stompClient.connect({token: myToken}, function (frame) { alert("连接成功"); }); } /* 订阅信息 */ function subscribeSocket(){ // 设置订阅地址 SUBSCRIBE = SUBSCRIBE_PREFIX + $("#subscribe").val(); // 输出订阅地址 alert("设置订阅地址为:" + SUBSCRIBE); // 执行订阅消息 stompClient.subscribe("/user" + SUBSCRIBE, function (responseBody) { var receiveMessage = JSON.parse(responseBody.body); console.log(receiveMessage); $("#information").append("<tr><td>" + receiveMessage.content + "</td></tr>"); }); } /* 断开连接 */ function disconnect() { stompClient.disconnect(function() { alert("断开连接"); }); } /* 发送消息并指定目标地址 */ function sendMessageNoParameter() { // 设置发送的内容 var sendContent = $("#content").val(); // 设置发送的用户 var sendUser = $("#targetUser").val(); // 设置待发送的消息内容 var message = '{"targetUser":"' + sendUser + '", "destination": "' + SUBSCRIBE + '", "content": "' + sendContent + '"}'; // 发送消息 stompClient.send(SEND_ENDPOINT, {}, message); }
创建用于展示 WebSocket 相关功能的 WEB HTML 页面 index.html,内容如下:
<!DOCTYPE html> <html> <head> <title>Hello WebSocket</title> <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/sockjs-client/1.4.0/sockjs.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script> <script src="app-websocket.js"></script> </head> <body> <div id="main-content" class="container" style="margin-top: 10px;"> <div class="row"> <form class="navbar-form" style="margin-left:0px"> <div class="col-md-12"> <div class="form-group"> <label>WebSocket 连接:</label> <button class="btn btn-primary" type="button" onclick="connect();">进行连接</button> <button class="btn btn-danger" type="button" onclick="disconnect();">断开连接</button> </div> <label>订阅地址:</label> <div class="form-group"> <input type="text" id="subscribe" class="form-control" placeholder="订阅地址"> </div> <button class="btn btn-warning" onclick="subscribeSocket();" type="button">订阅</button> </div> </form> </div> </br> <div class="row"> <div class="form-group"> <label>TOKEN 信息:</label> <input type="text" id="myToken" class="form-control" placeholder="TOKEN 信息"> <label>发送的目标用户:</label> <input type="text" id="targetUser" class="form-control" placeholder="发送的用户"> <label for="content">发送的消息内容:</label> <input type="text" id="content" class="form-control" placeholder="消息的内容"> </div> <button class="btn btn-info" onclick="sendMessageNoParameter();" type="button">发送</button> </div> </br> <div class="row"> <div class="col-md-12"> <h5 class="page-header" style="font-weight:bold">接收到的消息:</h5> <table class="table table-striped"> <tbody id="information"></tbody> </table> </div> </div> </div> </body> </html>
为了方便测试,需要打开两个不同类型浏览器(这里模拟通过 Header 传 Token 的方式进行用户验证,具体登录逻辑不实现,而是直接使用事先配置好的两个用户 Token 进行模拟)来进行测试,两个浏览器同时输入地址 http://localhost:8080/index.html 访问测试的前端页面 /index.html
如下:
两个浏览器中都执行下面步骤进行测试:
执行完上面步骤成后,可以在两个不同浏览器中观察到如下内容:
这里使用 Maven 工具管理依赖包,分别引入下面依赖:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.1.RELEASE</version> </parent> <groupId>mydlq.club</groupId> <artifactId>springboot-websocket-queue-security-example</artifactId> <version>0.0.1</version> <name>springboot-websocket-queue-security-example</name> <description>Spring Boot WebSocket Example</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <!-- SpringBoot WebSocket --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
import lombok.Data; @Data public class MessageBody { /** 发送消息的用户 */ private String from; /** 消息内容 */ private String content; /** 目标用户(告知 STOMP 代理转发到哪个用户) */ private String targetUser; /** 广播转发的目标地址(告知 STOMP 代理转发到哪个地方) */ private String destination; }
import lombok.Data; @Data public class User { private String username; private String password; private String name; private String sex; }
WebSocket 握手请求拦截器,可用于拦截握手请求和响应,以及将 HTTP 属性传递到目标 WebSocketHandler,这里主要是设置捂手时候,从 HTTP Session 中获取用户信息,传入 WebSocket 信息中。
import mydlq.club.model.User; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.HandshakeInterceptor; import javax.servlet.http.HttpSession; import java.util.Map; public class HttpHandshakeInterceptor implements HandshakeInterceptor { /** * 握手前拦截,从 HTTP 中参数传入 WebSocket Attributes 方便后续取出相关参数 * * @param request 请求对象 * @param response 响应对象 * @param wsHandler WebSocket 处理器 * @param attributes 从 HTTP 握手到与 WebSocket 会话关联的属性 * @return 如果返回 true 则继续握手,返回 false 则终止握手 */ @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) { // 将 request 对象转换为 ServletServerHttpRequest 对象 ServletServerHttpRequest serverRequest = (ServletServerHttpRequest) request; // 获取 HTTP Session 对象 HttpSession session = serverRequest.getServletRequest().getSession(); if (session != null) { // 从 HTTP Session 中获取用户信息 User user = (User) session.getAttribute("user"); if (user != null) { // 将从 HTTP Session 中获取的用户信息存入 WebSocket 的 Attributes 对象中 attributes.put("user", user); // 继续握手 return true; } } // 终止握手 return false; } /** * 握手完成后调用 * * @param request 请求对象 * @param response 响应对象 * @param wsHandler WebSocket 处理器 * @param ex 异常信息 */ @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) { } }
创建用于与正在建立会话过程中的 WebSocket 的用户相关联的方法,可以在此处配置用于关联用户相关的信息。
import mydlq.club.model.User; import org.springframework.http.server.ServerHttpRequest; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.server.support.DefaultHandshakeHandler; import java.security.Principal; import java.util.Map; public class HttpHandshakeHandler extends DefaultHandshakeHandler { /** * 用于与正在建立会话过程中的 WebSocket 的用户相关联的方法,可以在此处配置进行关联的用户信息。 * * @param request 握手请求对象 * @param wsHandler WebSocket 处理器 * @param attributes 从 HTTP 握手到与 WebSocket 会话关联的属性 * @return 对于 WebSocket 的会话的用户或者 null */ @Override protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) { User user = (User) attributes.get("user"); return user::getUsername; } }
创建 WebSocket 配置类,配置进行连接注册的端点 /mydlq
和消息代理前缀 /queue
及接收客户端发送消息的前缀 /app
,再加一个 configureWebSocketTransport
配置,用于设置 WebSocket 消息交互配置,这里可以将上面创建的。
import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; /** * @author mydlq */ @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { /** * 配置消息代理选项 * * @param registry 消息代理注册配置 */ @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 设置一个或者多个代理前缀,在 Controller 类中的方法里面发生的消息,会首先转发到代理从而发送到对应广播或者队列中。 registry.enableSimpleBroker("/queue"); // 配置客户端发送请求消息的一个或多个前缀,该前缀会筛选消息目标转发到 Controller 类中注解对应的方法里 registry.setApplicationDestinationPrefixes("/app"); // 服务端通知客户端的前缀,可以不设置,默认为user registry.setUserDestinationPrefix("/user"); } /** * 配置 WebSocket 进入点,及开启使用 SockJS,这些配置主要用配置连接端点,用于 WebSocket 连接 * * @param registry STOMP 端点 */ @Override public void registerStompEndpoints(StompEndpointRegistry registry) { // 配置 websocket 进入点 registry.addEndpoint("/mydlq") .addInterceptors(new HttpHandshakeInterceptor()) .setHandshakeHandler(new HttpHandshakeHandler()) .withSockJS(); } }
跟上面介绍广播模式一样,作用也是根据 WebSocket 配置类中 /app 前缀匹配后进入 Controller 类进行逻辑处理操作。
import mydlq.club.model.MessageBody; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.SimpMessageSendingOperations; import org.springframework.stereotype.Controller; import java.security.Principal; /** * @author mydlq */ @Controller public class MessageController { @Autowired private SimpMessageSendingOperations simpMessageSendingOperations; /** * 点对点发送消息,将消息发送到指定用户 */ @MessageMapping("/test") public void sendUserMessage(Principal principal, MessageBody messageBody) { messageBody.setFrom(principal.getName()); simpMessageSendingOperations.convertAndSendToUser(messageBody.getTargetUser(), messageBody.getDestination(), messageBody); } }
创建一个用于登录的 Controller 类,这里提前创建了两个模拟用户 mydlq1
与 mydlq2
与登录接口 /login
,这里设置登录成功后就将用户信息存储到 Session 中,类内容如下:
import mydlq.club.model.User; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.annotation.PostConstruct; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Map; @RestController public class LoginController { /** * 设置存储的 SESSION 名称 */ public static final String USER_SESSION_NAME = "user"; /** * 设置模拟用户信息 */ private static final Map<String, User> USER_MAP = new HashMap<>(); /** * 初始化 */ @PostConstruct public void init() { User user1 = new User(); user1.setUsername("mydlq1"); user1.setPassword("123456"); User user2 = new User(); user2.setUsername("mydlq2"); user2.setPassword("123456"); USER_MAP.put(user1.getUsername(), user1); USER_MAP.put(user2.getUsername(), user2); } @PostMapping("/login") public String login(@RequestParam String username, @RequestParam String password, HttpServletRequest request, HttpServletResponse response) throws IOException { // 检测是否已经登录 if (request.getSession().getAttribute(USER_SESSION_NAME) != null) { return "已经登陆,无需重复登陆"; } // 验证用户名密码,如果验证失败则返回错误信息 if (validate(username, password)) { // 验证成功则将登录用户信息存储 request.getSession().setAttribute(USER_SESSION_NAME, USER_MAP.get(username)); // 跳转到首页 response.sendRedirect("/index.html"); } return "用户名或密码不正确"; } /** * 验证用户名、密码是否正确 * * @param username 用户名 * @param password 密码 */ private boolean validate(String username, String password) { // 验证用户名密码,如果验证失败则返回错误信息 for (User user : USER_MAP.values()) { if (user.getUsername().equals(username) && user.getPassword().equals(password)) { return true; } } return false; } }
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
创建用于操作 WebSocket 的 JS 文件 app-websocket.js,内容如下:
// 设置 STOMP 客户端 var stompClient = null; // 设置 WebSocket 进入端点 var SOCKET_ENDPOINT = "/mydlq"; // 设置订阅消息的请求地址前缀 var SUBSCRIBE_PREFIX = "/queue"; // 设置订阅地址 var SUBSCRIBE = ""; // 设置服务器端点,访问服务器中哪个接口 var SEND_ENDPOINT = "/app/test"; /* 进行连接 */ function connect() { // 设置 SOCKET var socket = new SockJS(SOCKET_ENDPOINT); // 配置 STOMP 客户端 stompClient = Stomp.over(socket); // STOMP 客户端连接 stompClient.connect({}, function (frame) { alert("连接成功"); }); } /* 订阅信息 */ function subscribeSocket(){ // 设置订阅地址 SUBSCRIBE = SUBSCRIBE_PREFIX + $("#subscribe").val(); // 输出订阅地址 alert("设置订阅地址为:" + SUBSCRIBE); // 执行订阅消息 stompClient.subscribe("/user" + SUBSCRIBE, function (responseBody) { var receiveMessage = JSON.parse(responseBody.body); console.log(receiveMessage); $("#information").append("<tr><td>" + receiveMessage.content + "</td></tr>"); }); } /* 断开连接 */ function disconnect() { stompClient.disconnect(function() { alert("断开连接"); }); } /* 发送消息并指定目标地址 */ function sendMessageNoParameter() { // 设置发送的内容 var sendContent = $("#content").val(); // 设置发送的用户 var sendUser = $("#targetUser").val(); // 设置待发送的消息内容 var message = '{"targetUser":"' + sendUser + '", "destination": "' + SUBSCRIBE + '", "content": "' + sendContent + '"}'; // 发送消息 stompClient.send(SEND_ENDPOINT, {}, message); }
创建用于展示 WebSocket 相关功能的 WEB HTML 页面 index.html,内容如下:
<!DOCTYPE html> <html> <head> <title>Hello WebSocket</title> <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/sockjs-client/1.4.0/sockjs.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script> <script src="app-websocket.js"></script> </head> <body> <div id="main-content" class="container" style="margin-top: 10px;"> <div class="row"> <form class="navbar-form" style="margin-left:0px"> <div class="col-md-12"> <div class="form-group"> <label>WebSocket 连接:</label> <button class="btn btn-primary" type="button" onclick="connect();">进行连接</button> <button class="btn btn-danger" type="button" onclick="disconnect();">断开连接</button> </div> <label>订阅地址:</label> <div class="form-group"> <input type="text" id="subscribe" class="form-control" placeholder="订阅地址"> </div> <button class="btn btn-warning" onclick="subscribeSocket();" type="button">订阅</button> </div> </form> </div> </br> <div class="row"> <div class="form-group"> <label>发送的目标用户:</label> <input type="text" id="targetUser" class="form-control" placeholder="发送的用户"> <label for="content">发送的消息内容:</label> <input type="text" id="content" class="form-control" placeholder="消息的内容"> </div> <button class="btn btn-info" onclick="sendMessageNoParameter();" type="button">发送</button> </div> </br> <div class="row"> <div class="col-md-12"> <h5 class="page-header" style="font-weight:bold">接收到的消息:</h5> <table class="table table-striped"> <tbody id="information"></tbody> </table> </div> </div> </div> </body> </html>
创建用登录能的 WEB HTML 页面 login.html,内容如下:
<!DOCTYPE html> <html> <head> <title>Hello WebSocket</title> <link href="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/sockjs-client/1.4.0/sockjs.min.js"></script> <script src="https://cdn.bootcdn.net/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script> </head> <body> <div class="Container"> <div class="row"> <div class="col-md-4 col-md-offset-4"> <h1 class="page-header">用户登录</h1> <form role="form" method="post" action="/login"> <div class="form-group"> <label for="username">用户名:</label> <input type="text" class="form-control" placeholder="请输入账号" id="username" name="username"> </div> <div class="form-group"> <label for="password">密 码:</label> <input type="password" class="form-control" placeholder="请输入密码" id="password" name="password"> </div> <div class="checkbox" style="text-align:center;"> <br/> <input type="submit" value="登录" class="btn"/> </div> </form> </div> </div> </div> </body> </html>
为了方便测试,需要打开两个不同类型浏览器(因为用户登录后会存 Session,如果一个浏览器不同用户登录会使之前 Session 失效)来进行测试,两个浏览器同时输入地址 http://localhost:8080/index.html 访问测试的前端页面,然后可以看到并没有进入 /index.html
页面,而是跳转到 Spring Security 提供的登录的 /login
页面,如下:
两个浏览器中都输入用户名/密码 mydlq1/123456
与 mydlq2/123456
进行登录,然后会回到 /index.html
页面,然后执行下面步骤进行测试:
执行完上面步骤成后,可以在两个不同浏览器中观察到如下内容:
import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/queue"); registry.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/mydlq") // 设置允许跨域,设置为"*"则为允许全部域名 .setAllowedOrigins("*") .withSockJS(); } }
import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.WebSocketHandlerDecorator; import org.springframework.web.socket.handler.WebSocketHandlerDecoratorFactory; @Configuration public class HttpWebSocketHandlerDecoratorFactory implements WebSocketHandlerDecoratorFactory { /** * 配置 webSocket 处理器 * * @param webSocketHandler webSocket 处理器 * @return webSocket 处理器 */ @Override public WebSocketHandler decorate(WebSocketHandler webSocketHandler) { return new WebSocketHandlerDecorator(webSocketHandler) { /** * websocket 连接时执行的动作 * @param session websocket session 对象 * @throws Exception 异常对象 */ @Override public void afterConnectionEstablished(final WebSocketSession session) throws Exception { // 输出进行 websocket 连接的用户信息 if (session.getPrincipal() != null) { String username = session.getPrincipal().getName(); System.out.println("用户:" + username + "上线"); super.afterConnectionEstablished(session); } } /** * websocket 关闭连接时执行的动作 * @param session websocket session 对象 * @param closeStatus 关闭状态对象 * @throws Exception 异常对象 */ @Override public void afterConnectionClosed(final WebSocketSession session, CloseStatus closeStatus) throws Exception { // 输出关闭 websocket 连接的用户信息 if (session.getPrincipal() != null) { String username = session.getPrincipal().getName(); System.out.println("用户:" + username + "下线"); super.afterConnectionClosed(session, closeStatus); } } }; } }
import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/queue"); registry.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/mydlq").withSockJS(); } /** * 添加 WebSocket 用户上、下线监听器 */ @Override public void configureWebSocketTransport(WebSocketTransportRegistration registry) { registry.addDecoratorFactory(new HttpWebSocketHandlerDecoratorFactory()); } }
如果对你有帮助,别忘了 Github 点颗星嘞~
—END—