springboot 統合 WebSocket 永続接続 (許可フィルタリング + インターセプト)


        Springboot は WebSocket に依存し、pom.xml: springboot の最小限の依存関係を統合し、サービス接続を行わず、プロジェクトを個別に開始します。

  <!-- springboot依赖maven仓库版本  -->
  <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>2.2.5.RELEASE</version>
  </parent>

<!-- SpringBoot通用依赖模块 -->
<dependencies>
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
  </dependency>
  <!--    -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <!--排除tomcat依赖: 准备替换undertow部署-->
        <exclusion>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <groupId>org.springframework.boot</groupId>
        </exclusion>
    </exclusions>
  </dependency>
  <!--undertow容器,替换springboot内置容器tomcat-->
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-undertow</artifactId>
  </dependency>
  <!--springboot WebSocket -->
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-websocket</artifactId>
  </dependency>
<dependencies>

1. WebSocket を使用する理由

        一般的なリクエストは HTTP リクエスト (一方向通信) であり、HTTP は短い接続 (非永続的) であり、通信はクライアントによってのみ開始できるため、HTTP プロトコルではサーバーがクライアントにメッセージをアクティブにプッシュすることはできません。例: フロントエンドとバックエンドの対話とは、フロントエンドがリクエストを送信し、バックエンドからデータを取得した後にページにデータを表示することを意味します。フロントエンドが積極的にインターフェイスをリクエストしない場合、バックエンドはフロントエンドにデータを送信できません。しかし、WebSocket はこの問題をうまく解決でき、サーバーはクライアントにメッセージをアクティブにプッシュでき、クライアントもサーバーにメッセージをアクティブに送信できるため、サーバーとクライアントの真の平等が実現されます。
画像.png

2. 設定方法1:ServletContextInitializer+@ServerEndpointアノテーションを実装する

2.1、WebSocket の設定

package com.chengfu.config;

import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;

@Configuration
public class WebSocketConfig implements ServletContextInitializer {
    
    

    //    原文链接:https://blog.csdn.net/weixin_44185837/article/details/124942482
    /**
     * 这个bean的注册,用于扫描带有@ServerEndpoint的注解成为websocket,如果你使用外置的tomcat就不需要该配置文件
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
    
    
        return new ServerEndpointExporter();
    }

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
    
    

    }
}

2.2、WebSocket 接続、@ServerEndpoint

package com.chengfu.socket;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * websocket服务端,接收websocket客户端长连接
 */
@ServerEndpoint("/websocket/api1/{id}")
@Component
@Slf4j
public class WebSocketServer {
    
    

    // 与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;

    // session集合,存放对应的session
    private static ConcurrentHashMap<Integer, Session> sessionPool = new ConcurrentHashMap<>();

    // concurrent包的线程安全Set,用来存放每个客户端对应的WebSocket对象。
    private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<>();

    /**
     * 建立WebSocket连接
     *
     * @param session
     * @param userId  用户ID
     */
    @OnOpen
    public void onOpen(Session session, @PathParam(value = "id") Integer userId) {
    
    
        log.info("WebSocket建立连接中,连接用户ID:{}", userId);
        try {
    
    
            Session historySession = sessionPool.get(userId);
            // historySession不为空,说明已经有人登陆账号,应该删除登陆的WebSocket对象
            if (historySession != null) {
    
    
                webSocketSet.remove(historySession);
                historySession.close();
            }
        } catch (IOException e) {
    
    
            log.error("重复登录异常,错误信息:" + e.getMessage(), e);
        }
        // 建立连接
        this.session = session;
        webSocketSet.add(this);
        sessionPool.put(userId, session);
        log.info("建立连接完成,当前在线人数为:{}", webSocketSet.size());
    }

    /**
     * 发生错误
     *
     * @param throwable e
     */
    @OnError
    public void onError(Throwable throwable) {
    
    
        throwable.printStackTrace();
    }

    /**
     * 连接关闭
     */
    @OnClose
    public void onClose() {
    
    
        webSocketSet.remove(this);
        log.info("连接断开,当前在线人数为:{}", webSocketSet.size());
    }

    /**
     * 接收客户端消息
     *
     * @param message 接收的消息
     */
    @OnMessage
    public void onMessage(String message) {
    
    
        log.info("收到客户端发来的消息:{}", message);
    }

    /**
     * 推送消息到指定用户
     *
     * @param userId  用户ID
     * @param message 发送的消息
     */
    public static void sendMessageByUser(Integer userId, String message) {
    
    
        log.info("用户ID:" + userId + ",推送内容:" + message);
        Session session = sessionPool.get(userId);
        try {
    
    
            session.getBasicRemote().sendText(message);
        } catch (IOException e) {
    
    
            log.error("推送消息到指定用户发生错误:" + e.getMessage(), e);
        }
    }

    /**
     * 群发消息
     *
     * @param message 发送的消息
     */
    public static void sendAllMessage(String message) {
    
    
        log.info("发送消息:{}", message);
        for (WebSocketServer webSocket : webSocketSet) {
    
    
            try {
    
    
                webSocket.session.getBasicRemote().sendText(message);
            } catch (IOException e) {
    
    
                log.error("群发消息发生错误:" + e.getMessage(), e);
            }
        }
    }
}

2.3、W​​ebSocket リクエストのフィルタリング

いくつかの情報を探したところ、Springboot が @ServerEndpoint アノテーションが付けられた WebSocket リクエストを処理することはわかりませんでしたが、サーブレット コンテナ テクノロジを使用して ws リクエストをフィルタリングできます。ws リクエストのインターセプトは、設定方法 2 によって実現できます。詳細については、以下を参照してください。

package com.chengfu.config;

import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Arrays;

/**
 * servlet级别请求过滤
 */
@Component
public class WebSocketFilterConfig implements Filter {
    
    

    // websocket请求过滤清单
    private static final String[] FILTER_LIST = {
    
    "/websocket/api1/"};

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    
    
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    
    
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        String servletPath = httpServletRequest.getServletPath();

        // anyMatch: 有一个条件满足就返回true
        boolean match = Arrays.stream(FILTER_LIST).anyMatch(servletPath::startsWith);
        // boolean match = Arrays.stream(FILTER_LIST).anyMatch(filterUrl -> servletPath.startsWith(filterUrl));

        // 符合ws请求的url开始校验,其他请求一概放过
        if (match) {
    
    
            String token = httpServletRequest.getHeader("token");
            if (StringUtils.isNotBlank(token)) {
    
    
                filterChain.doFilter(servletRequest, servletResponse);
            }
//            else {
    
    
//                JSONObject result = new JSONObject();
//                HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
//                httpServletResponse.setContentType("application/json;charset=utf-8");
//                httpServletResponse.setCharacterEncoding("utf-8");
//                PrintWriter writer = httpServletResponse.getWriter();
//                writer.write(result.toJSONString());
//                writer.flush();
//                writer.close();
//            }
        } else {
    
    
            // 不包含过滤清单,直接放过请求
            filterChain.doFilter(servletRequest, servletResponse);
        }

    }

    @Override
    public void destroy() {
    
    
        Filter.super.destroy();
    }
}

2.4、郵便配達員がクライアント接続を確立する

画像.png
画像.png
接続に成功しました!
画像.png

3. 設定方法2:WebSocketConfigurer実装+TextWebSocketHandler継承

3.1. 設定: WebSocketConfigurer を実装する

package com.chengfu.config;

import com.chengfu.interceptor.WebSocketAuthInterceptor;
import com.chengfu.socket.WebSocketServer2;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

// 启用配置
@Configuration
// 启用websocket服务端
@EnableWebSocket
public class WebSocketConfig2 implements WebSocketConfigurer {
    
    

    // 原文链接:https://blog.csdn.net/weixin_44185837/article/details/124942482
    // 实现自: WebSocketConfigurer
    // 注册websocket拦截器
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
    
    
        webSocketHandlerRegistry
                // 只有符合"/websocket/api2/**", "/websocket/api3/**"的请求url,才能进入WebSocketServer2的服务端连接进行数据处理
                .addHandler(new WebSocketServer2(), "/websocket/api2/**", "/websocket/api3/**")
                // WebSocketServer2的握手拦截器处理:尽量避免被无用的请求攻击,在建立连接的时候通过检查授权成功之后才能进行访问
                .addInterceptors(new WebSocketAuthInterceptor())
                // 允许跨域访问
                .setAllowedOrigins("*");
    }
}

3.2. 構成: WebSocket リクエストのインターセプトを実現するための WebSocket ハンドシェイク

package com.chengfu.interceptor;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

import java.util.List;
import java.util.Map;

/**
 * websocket握手拦截
 */
@Component
@Slf4j
public class WebSocketAuthInterceptor implements HandshakeInterceptor {
    
    

    /**
     * 返回true允许直接通过,返回false拒绝连接
     *
     * @param serverHttpRequest
     * @param serverHttpResponse
     * @param webSocketHandler
     * @param map
     * @return
     * @throws Exception
     */
    @Override
    public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
    
    
        log.error("握手开始!");
        // 确保授权正确才能进行websocket连接
        HttpHeaders headers = serverHttpRequest.getHeaders();
        List<String> header = headers.get("token");
        if (header == null || header.size() == 0) {
    
    
            return false;
        }

        String token = header.get(0);
        if (StringUtils.isBlank(token)) {
    
    
            return false;
        }
        // TODO: token do something
        log.error("握手token:{}", token);
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
    
    
        log.error("握手结束");
    }
}

3.3、WebSocket サービスを実装し、ソケット クライアントの接続を監視します

package com.chengfu.socket;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.concurrent.ConcurrentHashMap;

// 配合webSocketConfig2使用
@Component
@Slf4j
public class WebSocketServer2 extends TextWebSocketHandler {
    
    

    /**
     * socket 建立成功事件 @OnOpen
     *
     * @param session
     * @throws Exception
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    
    
        // websocket入参
        // ===============从url上面获取参数
        String rawPath = session.getUri().getRawPath(); //
        String rawQuery = session.getUri().getRawQuery();//  从url上面获取参数
        String query = session.getUri().getQuery(); //  从url上面获取参数
        // =================

        // ================从header上面获取参数
        HttpHeaders headers = session.getHandshakeHeaders(); // 从header上获取参数
        // ================

        String token = headers.get("token").get(0);
        if (token != null) {
    
    
            WebSocketSession s = WebSessionManager.get(token);
            if (s != null) {
    
    
                // 当前用户之前已经建立连接,关闭
                WebSessionManager.remove(token);
            }
            // 重新建立session
            WebSessionManager.add(token, session);
            log.error("建立websocket连接:{}", token);
        }

    }

    /**
     * 接收消息事件 @OnMessage
     *
     * @param session
     * @param message
     * @throws Exception
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
    
    
        // 获得客户端传来的消息
        String payload = message.getPayload();
        System.out.println("server 接收到发送的消息 " + payload);
        // 服务端发送回去
        session.sendMessage(new TextMessage("server 发送消息 " + payload + " " + LocalDateTime.now()));
    }

    /**
     * socket 断开连接时 @OnClose
     *
     * @param session
     * @param status
     * @throws Exception
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
    
    
        Object token = session.getAttributes().get("token");
        if (token != null) {
    
    
            // 用户退出,移除缓存
            WebSessionManager.remove(token.toString());
        }
    }
}

// 来源:https://www.cnblogs.com/meow-world/articles/16283492.html
// websocket的session管理器
class WebSessionManager {
    
    
    /**
     * 保存连接 session 的地方
     */
    private static final ConcurrentHashMap<String, WebSocketSession> SESSION_POOL = new ConcurrentHashMap<>();

    /**
     * 添加 session
     *
     * @param key
     */
    public static void add(String key, WebSocketSession session) {
    
    
        // 添加 session
        SESSION_POOL.put(key, session);
    }

    /**
     * 删除 session,会返回删除的 session
     *
     * @param key
     * @return
     */
    public static WebSocketSession remove(String key) {
    
    
        // 删除 session
        return SESSION_POOL.remove(key);
    }

    /**
     * 删除并同步关闭连接
     *
     * @param key
     */
    public static void removeAndClose(String key) {
    
    
        WebSocketSession session = remove(key);
        if (session != null) {
    
    
            try {
    
    
                // 关闭连接
                session.close();
            } catch (IOException e) {
    
    
                // todo: 关闭出现异常处理
                e.printStackTrace();
            }
        }
    }

    /**
     * 获得 session
     *
     * @param key
     * @return
     */
    public static WebSocketSession get(String key) {
    
    
        // 获得 session
        return SESSION_POOL.get(key);
    }
}

3.4、ポストマン テスト、WebSocket リクエストの開始

注記!

1. postman のバージョンを 10 以降にアップグレードする必要があります。ここでテストしたバージョンは v10.17.1 です。
2. Ctrl+N キーを押してリクエストを作成するための小さなウィンドウを開き、WebSocket を選択します。

画像.png
Postman リクエスト アドレス:
画像.png
        WebSocket 接続を確立する前に、ハンドシェイク処理を実行し、ヘッダー パラメーターを確認し、要件を満たしている場合は接続の確立を開始します。
画像.png
        インターセプトにより、ヘッダーにトークン識別子がないことが判明し、クライアント接続リクエストが拒否されました。トークンがリクエスト ヘッダーに追加された後、再リクエストします。トークンが検出された後、接続は正常に確立されました
画像.png
画像.png
        。
画像.png

参考リンク

  1. SpringBoot は WebSocket を使用します_springboot websocket_天の川を眺めるブログ - CSDN ブログ
  2. Springboot - interceptor_springboot interceptor_Brown Bear のブログが大好きです - CSDN ブログ
  3. Websocket クラスターと通信 (インターセプターを含む) を実現する 2 番目の方法 - websockethandler_Second Xiaoer のブログ - CSDN ブログ
  4. Springboot が WebSocket ハンドシェイク インターセプターを統合 - meow_world - 博客园
  5. Postman は WebSocket を使用して request_w3cschool

おすすめ

転載: blog.csdn.net/dongzi_yu/article/details/132279345