SpringBoot Combat (17) | Интеграция WebSocket для реализации чата

Spring Boot

Публичный аккаунт WeChat: отличная трата времени. Если у вас есть какие-либо вопросы, пожалуйста, оставьте сообщение в фоновом режиме, я все равно не буду слушать.

предисловие

Во вчерашней статье была представлена ​​реализация WebSocket-трансляции, то есть при наличии сообщения на стороне сервера сообщение отправляется во все браузеры, подключенные к текущей конечной точке. Но это не решает проблему того, кто отправляет сообщение и кто его получает. Итак, напишите статью сегодня, чтобы реализовать чат один на один.

Сегодняшняя статья основана на вчерашней статье.Чтобы лучше понять сегодняшнюю статью, рекомендуется сначала прочитать ее: "SpringBoot интегрирует WebSocket для реализации широковещательных сообщений."

Готов к работе

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

пом-зависимости

Поскольку чат связан с пользователями, зависимость Spring Security 2.1.3 RELEASE введена на основе предыдущей статьи.

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

Конфигурация безопасности Spring

Хотя он включает в себя 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**");
    }
}

Конфигурация веб-сокетов

На основе предыдущей статьи регистрируется еще один узел с именем "/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");
    }
}

контроллер

Укажите формат и шаблон отправляемого сообщения. Подробности смотрите в комментариях к коду.

@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
public class ViewController {

    @GetMapping("/nasus")
    public String getView(){
        return "nasus";
    }

    @GetMapping("/login")
    public String getLoginView(){
        return "login";
    }

    @GetMapping("/chat")
    public String getChatView(){
        return "chat";
    }

}

тестовое задание

Ожидаемый результат должен быть таким: два пользователя входят в систему и могут отправлять друг другу сообщения. Однако сеанс сеанса пользователя того же браузера является общим, и в браузере Chrome необходимо добавить другого пользователя.

Конкретная операция находится в настройках Chrome -> Управление пользователями -> Добавить пользователя:

谷歌浏览器添加用户

Два пользователя посещают http://localhost:8080/login, чтобы войти в систему и перейти к интерфейсу чата:

聊天界面

Отправляйте сообщения друг другу:

互发消息

полный код

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

Если вы считаете, что это полезно для вас, пожалуйста, поставьте звезду и уходите, большое спасибо.

послесловие

Если эта статья поможет вам хоть немного, пожалуйста, помогите. Твоя внешность мотивирует меня продолжать писать.

Кроме того, я отправляю его после того, как обратил внимание1024Доступны бесплатные учебные материалы.

Подробнее см. в этой старой статье:Python, C++, Java, Linux, Go, внешний интерфейс, алгоритм обмена данными

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