添加依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-websocketartifactId>
<version>2.1.7.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
<version>2.1.8.RELEASEversion>
dependency>
/**
* 配置WebSocket
* 广播式 (将消息发送给所有连接了当前endpoint的浏览器)
*/
@Configuration
@EnableWebSocketMessageBroker //注解开启使用STOMP协议来传输基于代理(message broker)的消息
public class WebsocketConfig implements WebSocketMessageBrokerConfigurer {
//注册STOMP协议的节点
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//映射指定的 URL,指定使用SockJS协议
registry.addEndpoint("/endpointWisely").withSockJS(); //注册STOMP协议的节点endpoint,并指定SockJS协议
}
//配置消息代理
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//广播式应该配置一个topic代理
registry.enableSimpleBroker("/topic");
}
}
(2)浏览器向服务端发送的消息用此类接收
public class WiselyMessage {
private String name;
public String getName() {
return name;
}
}
(3)服务端向浏览器发送此类的消息
public class WiselyResponse {
private String responseMessage;
public WiselyResponse(String responseMessage) {
this.responseMessage = responseMessage;
}
public String getResponseMessage() {
return responseMessage;
}
}
(4) 控制器示例
@Controller
public class WsController {
//1 通过SimpMessagingTemplate 向浏览器发送消息
@Autowired
private SimpMessagingTemplate messagingTemplate;
@MessageMapping("/welcome") //类似RequestMapping,地址映射
@SendTo("/topic/getResponse")//当服务端有消息时,会对订阅了@SendTo中的路径的浏览器发送消息
public WiselyResponse say(WiselyMessage message) throws Exception{
Thread.sleep(3000);
return new WiselyResponse("welcome,"+ message.getName()+"!");
}
}
(5)添加脚本
(6) 演示页面 在src/main/resources/templates下新建 ws.html, 下载js包,添加thymeleaf依赖
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org" xmlns="http://www.w3.org/1999/xhtml">
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
<script th:src="@{/static/jquery-3.3.1.js}" type="text/javascript">script>
<script th:src="@{/static/sockjs.min.js}" type="text/javascript">script>
<script th:src="@{/static/stomp.min.js}" type="text/javascript">script>
<head>
<title>Spring Boot+WebSocket+广播式title>
head>
<body onload="disconnect()">
<noscript><h2 style="color: #ff0000">貌似你的浏览器不支持websocketh2>noscript>
<div>
<div>
<button id="connect" onclick="connect();">连接button>
<button id="disconnect" disabled="disabled" onclick="disconnect();">断开连接button>
div>
<div id="conversationDiv">
<label>输入你的名字label><input type="text" id="name" />
<button id="sendName" onclick="sendName();">发送button>
<p id="response">p>
div>
div>
<script type="text/javascript">
var stompClient = null;
function setConnected(connected) {
document.getElementById('connect').disabled = connected;
document.getElementById('disconnect').disabled = !connected;
document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';
$("#response").html();
}
function connect() {
var socket = new SockJS('/endpointWisely'); //1 连接SockJS的endpoint名称为 “/endpointWisely”
stompClient = Stomp.over(socket); //2 使用STOMP子协议WebSocket客户端
//3 连接WebSocket服务端
stompClient.connect({}, function(frame) {
setConnected(true);
console.log('Connected: ' + frame);
//4 订阅服务端发送的消息,这个是在控制器的@SendTo中定义的
stompClient.subscribe('/topic/getResponse', function(respnose){ //2
showResponse(JSON.parse(respnose.body).responseMessage);
});
});
}
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
function sendName() {
var name = $('#name').val();
//5 向服务端接口发送消息,这个在控制器@MessageMapping中定义的
stompClient.send("/welcome", {}, JSON.stringify({ 'name': name }));
}
function showResponse(message) {
var response = $("#response");
response.html(message);
}
script>
body>
html>
(7)配置viewController
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override
public void addViewControllers(ViewControllerRegistry registry){
//访问localhost:8787/ws 跳转ws.html页面
registry.addViewController("/ws").setViewName("/ws");
}
}
ps:开启3个浏览器窗口,并访问http://localhost:8787/ws
@Configuration
@EnableWebSecurity //
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.
authorizeRequests()
//1 对/ 和 /login 路径不拦截
.antMatchers("/","/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
//2 设置登录页面的路径
.loginPage("/login")
//3 成功后跳转至chat路径
.defaultSuccessUrl("/chat")
.permitAll()
.and()
.logout()
.permitAll();
}
//4 在内存中配置两个用户名、密码、角色,定义认证用于信息获取来源以及密码校验规则
//认证信息获取来源是内存获取——inMemoryAuthentication
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
//代码报错 There is no PasswordEncoder mapped for the id “null”
//https://blog.csdn.net/canon_in_d_major/article/details/79675033
//Spring security 5.0中新增了多种加密方式,也改变了密码的格式
//要将前端传过来的密码进行某种方式加密,spring security 官方推荐的是使用bcrypt加密方式
/*auth
.inMemoryAuthentication()
.withUser("mmz").password("123456").roles("USER")
.and()
.withUser("wisely").password("123456").roles("USER");*/
auth
.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("gdg").password(new BCryptPasswordEncoder().encode("123456")).roles("USER")
.and()
.withUser("yq").password(new BCryptPasswordEncoder().encode("123456")).roles("USER");
}
//5 指定路径下的静态资源不拦截
@Override
public void configure(WebSecurity webSecurity) throws Exception{
webSecurity.ignoring().antMatchers("/resources/static/**");
}
}
(3) 配置WebSocket
/**
* 配置WebSocket
* 广播式 (将消息发送给所有连接了当前endpoint的浏览器)
*/
@Configuration
@EnableWebSocketMessageBroker //注解开启使用STOMP协议来传输基于代理(message broker)的消息
public class WebsocketConfig implements WebSocketMessageBrokerConfigurer {
//注册STOMP协议的节点
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//映射指定的 URL,指定使用SockJS协议
registry.addEndpoint("/endpointWisely").withSockJS();
// 1 注册一个新的endpoint
registry.addEndpoint("/endpointChat").withSockJS();
}
//配置消息代理
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
//广播式应该配置一个topic代理
registry.enableSimpleBroker("/queue","/topic"); //2 点对点增加一个消息代理 queue
}
}
(4) 控制器示例
@Controller
public class WsController {
//1 通过SimpMessagingTemplate 向浏览器发送消息
@Autowired
private SimpMessagingTemplate messagingTemplate;
@MessageMapping("/chat")
public void handleChat(Principal principal, String msg){ //2 springMVC 中可以直接在参数中获取principal,它包含了当前用户的信息
//3 如果发送人是mmz,则发送给wisely,暂时硬编码测试,生产中看情况
if(principal.getName().equals("gdg")){
//4 向用户发送消息,convertAndSendToUser(接收消息的用户,浏览器的订阅地址,消息体)
messagingTemplate.convertAndSendToUser("yq","/queue/notifications",principal.getName() + "-send:" + msg);
}else {
messagingTemplate.convertAndSendToUser("gdg","/queue/notifications",principal.getName() + "-send:" + msg);
}
}
}
(5) 登录页面 在src/main/resources/templates下新建 login.html,
<html lang="zh-CN"
xmlns:th="http://www.thymeleaf.org" xmlns="http://www.w3.org/1999/xhtml"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
<head>
<title>登陆页面title>
head>
<body>
<div th:if="${param.error}">
无效的账号和密码
div>
<div th:if="${param.logout}">
你已注销
div>
<form th:action="@{/login}" method="post">
<div><label> 账号 : <input type="text" name="username"/> label>div>
<div><label> 密码: <input type="password" name="password"/> label>div>
<div><input type="submit" value="登陆"/>div>
form>
body>
html>
(6)聊天页面 在src/main/resources/templates下新建 chat.html,
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org" xmlns="http://www.w3.org/1999/xhtml">
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
<head>
<title>Hometitle>
<script th:src="@{/static/jquery-3.3.1.js}" type="text/javascript">script>
<script th:src="@{/static/sockjs.min.js}" type="text/javascript">script>
<script th:src="@{/static/stomp.min.js}" type="text/javascript">script>
head>
<body>
<p>
聊天室
p>
<form id="wiselyForm">
<textarea rows="4" cols="60" name="text">textarea>
<input type="submit"/>
form>
<script th:inline="javascript">
$('#wiselyForm').submit(function(e){
e.preventDefault();
var text = $('#wiselyForm').find('textarea[name="text"]').val();
sendSpittle(text);
});
var sock = new SockJS("/endpointChat"); //1 连接endpoint
var stomp = Stomp.over(sock);
stomp.connect('guest', 'guest', function(frame) {
//2 订阅 /user/queue/notifications 发送的消息,要求与messagingTemplate 中定义的地址一样,多了个user,且是必须的,这样才能把消息发送到指定的用户
stomp.subscribe("/user/queue/notifications", handleNotification);
});
function handleNotification(message) {
$('#output').append("Received: " + message.body + "
")
}
function sendSpittle(text) {
stomp.send("/chat", {}, text);//3
}
$('#stop').click(function() {sock.close()});
script>
<div id="output">div>
body>
html>
(7)增加页面的viewController
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override
public void addViewControllers(ViewControllerRegistry registry){
//访问localhost:8787/ws 跳转ws.html页面
registry.addViewController("/ws").setViewName("/ws");
registry.addViewController("/login").setViewName("/login");
registry.addViewController("/chat").setViewName("/chat");
}
/**
* 自动配置静态资源
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations(ResourceUtils.CLASSPATH_URL_PREFIX + "/static/");
super.addResourceHandlers(registry);
}
}
ps:打开两个浏览器窗口,谷歌浏览器可设置两个独立用户,然后访问http://localhost:8787/login
登录 gdg/123456,yq/123456 ,开启郭德纲和于谦私聊。