SpringBoot actual combat (17) | integration of WebSocket to realize chat room

  java-web, spring-security, springboot, websocket

WeChat Public Number: An Outstanding Disabled Person. If you have any questions, please leave a message backstage. I won’t listen anyway.

Preface

Yesterday’s article introduced the implementation of WebSocket broadcasting, that is, when the server side has a message, it sends the message to all browsers connected to the current endpoint. However, this cannot solve the problem of who sends the message and who receives it. So, today, write a chat room to realize one-on-one communication.

Today’s article is based on yesterday’s one. In order to better understand today’s article, it is recommended to read it first. “SpringBoot integrates WebSocket to realize broadcast message

Preparatory work

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

Pom dependency

Because chat rooms involve users, Spring Security 2.1.3 RELEASE dependency is introduced on the basis of the previous article.

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

Configuration of Spring Security

Although Spring Security is involved, due to the limited space, only relevant parts of this project will be introduced here. The specific Spring Security tutorial will be published later.

Spring Security configuration here is very simple, specifically setting login path, setting security resources, and creating users and passwords in memory. Passwords need to be encrypted. BCrypt encryption algorithm is used here to encrypt passwords when users log in. The code comments are very detailed, not much said.

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**");
    }
}

Configuration of WebSocket

On the basis of the previous article, another node named “/Endpointhat” is registered for users to subscribe to. Only users who subscribe to this node can receive messages. Then, add a message agent named “/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

Specify the format and template for sending messages. For details, see the code notes.

@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);
    }
}

Login page

<!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>

Chat page

<!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>

Page 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";
    }

}

test

The expected result should be that two users log into the system and can send messages to each other. However, the session of the user session of the same browser is shared, and another user needs to be added to Chrome browser.

The specific operation is in Chrome settings-> manage users-> add users:

谷歌浏览器添加用户

The two users visit separatelyhttp://localhost: 8080/login login system, jump to chat interface:

聊天界面

Send messages to each other:

互发消息

Complete code

https://github.com/turoDog/De …

If you think it is helpful to you, please give a Star before you leave. thank you very much.

Postscript

If this article is of any help to you, please help me look good. Your good looks are my motivation to persist in writing.

In addition, after the attention is sent1024Free study materials are available.

For details, please refer to this old article:Python, C++, Java, Linux, Go, Front End, Algorithm Data Sharing

一个优秀的废人,给你讲几斤技术