微信公众号:一个优秀的废人。如有问题,请后台留言,反正我也不会听。

前言

昨天那篇介绍了 WebSocket 实现广播,也即服务器端有消息时,将消息发送给所有连接了当前 endpoint 的浏览器。但这无法解决消息由谁发送,又由谁接收的问题。所以,今天写一篇实现一对一的聊天室。

今天这一篇建立在昨天那一篇的基础之上,为便于更好理解今天这一篇,推荐先阅读:「SpringBoot 整合WebSocket 实现广播消息

准备工作

  • Spring Boot 2.1.3 RELEASE
  • Spring Security 2.1.3 RELEASE
  • IDEA
  • JDK8

pom 依赖

因聊天室涉及到用户相关,所以在上一篇基础上引入 Spring Security 2.1.3 RELEASE 依赖

<!-- Spring Security 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

Spring Security 的配置

虽说涉及到 Spring Security ,但鉴于篇幅有限,这里只对这个项目相关的部分进行介绍,具体的 Spring Security 教程,后面会出。

这里的 Spring Security 配置很简单,具体就是设置登录路径、设置安全资源以及在内存中创建用户和密码,密码需要注意加密,这里使用 BCrypt 加密算法在用户登录时对密码进行加密。 代码注释很详细,不多说。

package com.nasus.websocket.config;

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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @Configuration
// 开启Spring Security的功能
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ @Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 设置 SpringSecurity 对 / 和 "/login" 路径不拦截
.mvcMatchers("/","/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
// 设置 Spring Security 的登录页面访问路径为/login
.loginPage("/login")
// 登录成功后转向 /chat 路径
.defaultSuccessUrl("/chat")
.permitAll()
.and()
.logout()
.permitAll();
} @Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
// 在内存中分配两个用户 nasus 和 chenzy ,用户名和密码一致
// BCryptPasswordEncoder() 是 Spring security 5.0 中新增的加密方式
// 登陆时用 BCrypt 加密方式对用户密码进行处理。
.passwordEncoder(new BCryptPasswordEncoder())
.withUser("nasus")
// 保证用户登录时使用 bcrypt 对密码进行处理再与内存中的密码比对
.password(new BCryptPasswordEncoder().encode("nasus")).roles("USER")
.and()
// 登陆时用 BCrypt 加密方式对用户密码进行处理。
.passwordEncoder(new BCryptPasswordEncoder())
.withUser("chenzy")
// 保证用户登录时使用 bcrypt 对密码进行处理再与内存中的密码比对
.password(new BCryptPasswordEncoder().encode("chenzy")).roles("USER");
} @Override
public void configure(WebSecurity web) throws Exception {
// /resource/static 目录下的静态资源,Spring Security 不拦截
web.ignoring().antMatchers("/resource/static**");
}
}

WebSocket 的配置

在上一篇的基础上另外注册一个名为 "/endpointChat" 的节点,以供用户订阅,只有订阅了该节点的用户才能接收到消息;然后,再增加一个名为 "/queue" 消息代理。

@Configuration
// @EnableWebSocketMessageBroker 注解用于开启使用 STOMP 协议来传输基于代理(MessageBroker)的消息,这时候控制器(controller)
// 开始支持@MessageMapping,就像是使用 @requestMapping 一样。
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//注册一个名为 /endpointNasus 的 Stomp 节点(endpoint),并指定使用 SockJS 协议。
registry.addEndpoint("/endpointNasus").withSockJS();
//注册一个名为 /endpointChat 的 Stomp 节点(endpoint),并指定使用 SockJS 协议。
registry.addEndpoint("/endpointChat").withSockJS();
} @Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 广播式配置名为 /nasus 消息代理 , 这个消息代理必须和 controller 中的 @SendTo 配置的地址前缀一样或者全匹配
// 点对点增加一个 /queue 消息代理
registry.enableSimpleBroker("/queue","/nasus/getResponse");
}
}

控制器 controller

指定发送消息的格式以及模板。详情见,代码注释。

@Autowired
//使用 SimpMessagingTemplate 向浏览器发送信息
private SimpMessagingTemplate messagingTemplate; @MessageMapping("/chat")
public void handleChat(Principal principal,String msg){
// 在 SpringMVC 中,可以直接在参数中获得 principal,principal 中包含当前用户信息
if (principal.getName().equals("nasus")){
// 硬编码,如果发送人是 nasus 则接收人是 chenzy 反之也成立。
// 通过 messageingTemplate.convertAndSendToUser 方法向用户发送信息,参数一是接收消息用户,参数二是浏览器订阅地址,参数三是消息本身
messagingTemplate.convertAndSendToUser("chenzy",
"/queue/notifications",principal.getName()+"-send:" + msg);
} else {
messagingTemplate.convertAndSendToUser("nasus",
"/queue/notifications",principal.getName()+"-send:" + msg);
}
}

登录页面

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<meta 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>

聊天页面

<!DOCTYPE html>

<html xmlns:th="http://www.thymeleaf.org">
<meta charset="UTF-8" />
<head>
<title>Home</title>
<script th:src="@{sockjs.min.js}"></script>
<script th:src="@{stomp.min.js}"></script>
<script th:src="@{jquery.js}"></script>
</head>
<body>
<p>
聊天室
</p> <form id="nasusForm">
<textarea rows="4" cols="60" name="text"></textarea>
<input type="submit"/>
</form> <script th:inline="javascript">
$('#nasusForm').submit(function(e){
e.preventDefault();
var text = $('#nasusForm').find('textarea[name="text"]').val();
sendSpittle(text);
}); // 连接 SockJs 的 endpoint 名称为 "/endpointChat"
var sock = new SockJS("/endpointChat");
var stomp = Stomp.over(sock);
stomp.connect('guest', 'guest', function(frame) {
// 订阅 /user/queue/notifications 发送的消息,这里与在控制器的
// messagingTemplate.convertAndSendToUser 中订阅的地址保持一致
// 这里多了 /user 前缀,是必须的,使用了 /user 才会把消息发送到指定用户
stomp.subscribe("/user/queue/notifications", handleNotification);
}); function handleNotification(message) {
$('#output').append("<b>Received: " + message.body + "</b><br/>")
} function sendSpittle(text) {
stomp.send("/chat", {}, text);
}
$('#stop').click(function() {sock.close()});
</script> <div id="output"></div>
</body>
</html>

页面控制器 controller

@Controller
public class ViewController { @GetMapping("/nasus")
public String getView(){
return "nasus";
} @GetMapping("/login")
public String getLoginView(){
return "login";
} @GetMapping("/chat")
public String getChatView(){
return "chat";
} }

测试

预期结果应该是:两个用户登录系统,可以互相发送消息。但是同一个浏览器的用户会话的 session 是共享的,这里需要在 Chrome 浏览器再添加一个用户。

具体操作在 Chrome 的 设置-->管理用户-->添加用户:

两个用户分别访问 http://localhost:8080/login 登录系统,跳转至聊天界面:

相互发送消息:

完整代码

https://github.com/turoDog/Demo/tree/master/springboot_websocket_demo

如果觉得对你有帮助,请给个 Star 再走呗,非常感谢。

后语

如果本文对你哪怕有一丁点帮助,请帮忙点好看。你的好看是我坚持写作的动力。

另外,关注之后在发送 1024 可领取免费学习资料。

资料详情请看这篇旧文:Python、C++、Java、Linux、Go、前端、算法资料分享

Spring Boot2 系列教程 (十七) | 整合 WebSocket 实现聊天室的更多相关文章

  1. Spring Boot2 系列教程(十七)SpringBoot 整合 Swagger2

    前后端分离后,维护接口文档基本上是必不可少的工作. 一个理想的状态是设计好后,接口文档发给前端和后端,大伙按照既定的规则各自开发,开发好了对接上了就可以上线了.当然这是一种非常理想的状态,实际开发中却 ...

  2. Spring Boot2 系列教程 (十三) | 整合 MyBatis (XML 版)

    前言 如题,今天介绍 SpringBoot 与 Mybatis 的整合以及 Mybatis 的使用,之前介绍过了 SpringBoot 整合MyBatis 注解版的使用,上一篇介绍过 MyBatis ...

  3. Spring Boot2 系列教程 (十一) | 整合数据缓存 Cache

    如题,今天介绍 SpringBoot 的数据缓存.做过开发的都知道程序的瓶颈在于数据库,我们也知道内存的速度是大大快于硬盘的,当需要重复获取相同数据时,一次又一次的请求数据库或者远程服务,导致大量时间 ...

  4. Spring Boot2 系列教程(二十)Spring Boot 整合JdbcTemplate 多数据源

    多数据源配置也算是一个常见的开发需求,Spring 和 SpringBoot 中,对此都有相应的解决方案,不过一般来说,如果有多数据源的需求,我还是建议首选分布式数据库中间件 MyCat 去解决相关问 ...

  5. Spring Boot2 系列教程(三十)Spring Boot 整合 Ehcache

    用惯了 Redis ,很多人已经忘记了还有另一个缓存方案 Ehcache ,是的,在 Redis 一统江湖的时代,Ehcache 渐渐有点没落了,不过,我们还是有必要了解下 Ehcache ,在有的场 ...

  6. Spring Boot2 系列教程 (十六) | 整合 WebSocket 实现广播

    前言 如题,今天介绍的是 SpringBoot 整合 WebSocket 实现广播消息. 什么是 WebSocket ? WebSocket 为浏览器和服务器提供了双工异步通信的功能,即浏览器可以向服 ...

  7. Spring Boot2 系列教程(十)Spring Boot 整合 Freemarker

    今天来聊聊 Spring Boot 整合 Freemarker. Freemarker 简介 这是一个相当老牌的开源的免费的模版引擎.通过 Freemarker 模版,我们可以将数据渲染成 HTML ...

  8. Spring Boot2 系列教程(二十六)Spring Boot 整合 Redis

    在 Redis 出现之前,我们的缓存框架各种各样,有了 Redis ,缓存方案基本上都统一了,关于 Redis,松哥之前有一个系列教程,尚不了解 Redis 的小伙伴可以参考这个教程: Redis 教 ...

  9. Spring Boot2 系列教程(二十二)整合 MyBatis 多数据源

    关于多数据源的配置,前面和大伙介绍过 JdbcTemplate 多数据源配置,那个比较简单,本文来和大伙说说 MyBatis 多数据源的配置. 其实关于多数据源,我的态度还是和之前一样,复杂的就直接上 ...

随机推荐

  1. Codeforces Round #194 (Div.1 + Div. 2)

    A. Candy Bags 总糖果数\(\frac{n^2(n^2+1)}{2}\),所以每人的数量为\(\frac{n}{2}(n^2+1)\) \(n\)是偶数. B. Eight Point S ...

  2. Vue v-if和v-show的使用.区别

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  3. java 九个预定义Class对象

    基本的 Java 类型(boolean.byte.char.short.int.long.float 和 double)和关键字 void通过class属性也表示为 Class 对象: Class类中 ...

  4. 【js】vue 2.5.1 源码学习(二) 策略合并

     一.  整体思路     1 首先是代码的大体构造,先判断引入代码的环境,即对应amd 和cmd的处理     2 vue_init 需要借助 initMinxin    ==>>> ...

  5. zookeeper(1)-概述

    ZooKeeper概述 ZooKeeper 是一个分布式的,开放源码的分布式应用程序协调服务,是 Google 的 Chubby 一个开源的实现.它提供了简单原始的功能,分布式应用可以基于它实现更高级 ...

  6. 【50.54%】【BZOJ 1879】[Sdoi2009]Bill的挑战

    Time Limit: 4 Sec  Memory Limit: 64 MB Submit: 649  Solved: 328 [Submit][Status][Discuss] Descriptio ...

  7. import()函数

    简介 import命令会被 JavaScript 引擎静态分析,先于模块内的其他模块执行(叫做”连接“更合适).所以,下面的代码会报错. // 报错 if (x === 2) { import MyM ...

  8. [板子]KMP

    KMP板子,你甚至可以用这个板子A掉luogu的3375 基础懒得说,要求一个Next数组. #include<cstdio> #include<algorithm> #inc ...

  9. 关于redis有序集合http://www.runoob.com/redis/redis-sorted-sets.html

    redis有序集合和集合一样,元素都是字符串类型,而且不能重复 和普通集合不同的是它关联一个double类型的分数,redis是同个元素的分数来对元素进行排序 有序集合的元素是唯一的,但是分数可以重复 ...

  10. Hibernate映射文件详解(News***.hbm.xml)一

    Hibernate是一个彻底的ORM(Object Relational Mapping,对象关系映射)开源框架. 我们先看一下官方文档所给出的,Hibernate 体系结构的高层视图: 其中PO=P ...