springboot integrated websocket persistent connection (permission filtering + interception)


        Springboot depends on websocket, pom.xml: integrates the minimum dependency of springboot, does not make any service connection, and starts the project separately

  <!-- 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. Why use WebSocket?

        Because general requests are HTTP requests (one-way communication), HTTP is a short connection (non-persistent), and communication can only be initiated by the client, the HTTP protocol cannot allow the server to actively push messages to the client. For example: front-end and back-end interaction means that the front-end sends requests, and displays the data on the page after getting the data from the back-end. If the front-end does not actively request the interface, the back-end cannot send data to the front-end. However, WebSocket can solve this problem very well. The server can actively push messages to the client, and the client can also actively send messages to the server, realizing the true equality between the server and the client.
image.png

2. Configuration method 1: Implement ServletContextInitializer+@ServerEndpoint annotation

2.1, WebSocket configuration

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 connection, @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, WebSocket request filtering

After looking for some information, I didn't find that springboot handles websocket requests annotated with @ServerEndpoint, but servlet container technology can be used to filter ws requests. The interception of ws requests can be realized through configuration method 2, see below for details.

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, postman establishes a client connection

image.png
image.png
connection succeeded!
image.png

3. Configuration method 2: implement WebSocketConfigurer + inherit TextWebSocketHandler

3.1. Configuration: implement 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. Configuration: WebSocket handshake to realize the interception of websocket requests

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, implement WebSocket service, monitor the connection of socket client

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, postman test, initiate websocket request

Note!

1. The version of postman needs to be upgraded to 10 or above. The version I tested here is v10.17.1;
2. Ctrl+N, open the small window for creating a request, and select WebSocket;

image.png
Postman request address:
image.png
        Before establishing a websocket connection, perform handshake processing, check the headers parameters, and start establishing a connection if it meets the requirements.
image.png
        The interception found that the headers did not have a token identifier, and rejected the client connection request. After the token was added to the request header, re-request:
image.png
image.png
        After the token was detected, the connection was successfully established:
image.png

reference link

  1. SpringBoot uses WebSocket_springboot websocket_Looking at the Milky Way Blog-CSDN Blog
  2. Springboot - interceptor_springboot interceptor_I love Brown Bear's Blog-CSDN Blog
  3. The second way to realize Websocket cluster and communication (including interceptor) - websockethandler_Second Xiaoer's Blog - CSDN Blog
  4. springboot integrates websocket handshake interceptor - meow_world - 博客园
  5. Postman uses WebSocket to request_w3cschool

Guess you like

Origin blog.csdn.net/dongzi_yu/article/details/132279345