美文网首页
基于springboot的websocket完整示例(包括鉴权,

基于springboot的websocket完整示例(包括鉴权,

作者: coder4j | 来源:发表于2022-01-23 00:50 被阅读0次

本篇文章从后端到前端完整的展示springboot基于注解的方式使用websocket,鉴权及前端js代码示例(心跳及断开重连)。

后端部分

服务搭建

引入websocket依赖包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
    <version>5.1.2.RELEASE</version>
</dependency>

创建Websocket类,并添加@ServerEndPoint注解,声明此类是一个ServerEndpoint服务

/**
 * 每个ws请求都会生成一个websocket实例
 */
@Data
@Slf4j
@ServerEndpoint(value = "/websocket/{sid}")
@Component
public class WebSocket {

    private String sid;
    private Session session;
    
    @OnOpen
    public void onOpen(@PathParam("sid") String sid, Session session) {
        log.info("WebSocket onOpen sid:{}", sid);
        this.sid = sid;
        this.session = session;
        WebSocketHolder.put(userNo, this);
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        log.info("WebSocket onClose sid:{}", sid);
        WebSocketHolder.remove(sid, this);
    }

    /**
     * 收到客户端消息后调用的方法
     * @param message 客户端发送过来的消息
     * @param session 可选的参数
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("WebSocket onMessage message: {}", message);
        if (message.equals("ping")) {
            sendMessage("pong");
        }
    }

    /**
     * 发生错误时调用
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        log.error("WebSocket onError");
    }

    /**
     * 发送消息
     * @param message
     */
    public void sendMessage(String message) {
        try {
            this.session.getBasicRemote().sendText(message);
        } catch (IOException e) {
            log.error("消息发送失败: {}", e.getMessage(), e);
        }
    }

    public void close() {
        try {
            this.session.close();
        } catch (IOException e) {
            log.error("session 关闭失败: {}", e.getMessage(), e);
        }
    }
    
}

要想保证后端和前端进行通讯,必须要将建立的会话(Session)缓存起来,因此我们创建WebSocketHolder类

@Slf4j
public class WebSocketHolder {
    // 用于缓存用户的会话,value之所以是一个集合,是为了保存同一个用户多终端登录的会话
    public static Map<String, Set<WebSocket>> holder = new ConcurrentHashMap<>();
    // 计数器,用于统计当前登录用户会话数
    public static AtomicInteger counter = new AtomicInteger();

    /**
     * 存储session
     * @param sid
     * @param session
     */
    public static void put(String sid, WebSocket session) {
        Set<WebSocket> sessions = holder.getOrDefault(sid, new HashSet<>());
        if (sessions.size() == 0) {
            holder.put(sid, sessions);
        }
        sessions.add(session);
        // 计数
        int c = counter.incrementAndGet();
        log.info("用户{}登录,当前在线会话为: {}", sid, c);
    }

    /**
     * 获取session
     * <p>
     * 1. sid不为空,则获取指定用户;<br/>
     * 2. sid为空,则获取所有登录用户;<br/>
     * </p>
     * @param sid
     * @return
     */
    public static Set<WebSocket> get(String sid) {
        Set<WebSocket> set = new HashSet<>();
        if (StringUtils.isEmpty(sid)) {
            // sid标识为空
            holder.values().forEach(s -> set.addAll(s));
        } else {
            // sid不为空
            if (holder.containsKey(sid)) {
                set.addAll(holder.get(sid));
            }
        }
        return set;
    }

    /**
     * 移除session
     * @param sid
     */
    public static void remove(String sid, WebSocket socket) {
        Set<WebSocket> sockets = holder.get(sid);
        socket.close();
        sockets.remove(socket);
        if (sockets.size() == 0) {
            holder.remove(sid);
        }
        int c = counter.decrementAndGet();
        log.info("用户{}退出,当前在线会话为: {}", sid, c);
    }
}

同时还需要创建一个配置类,用于实例化ServerEndpointExporter,此类会自动注册上面定义的EndPointer

@Configuration
public class WebSocketConfigurator  {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
    
}

这样后端基本的websocket服务就搭建起来了。

鉴权

出于安全考虑,往往需要对请求进行鉴权,因此需要获取前端传递的token,获取的方式目前想到的有3种,仅供参考:

  1. 类似于sid参数,将token放到请求路径中,然后在onOpen中对token进行鉴权;
  2. 建立链接后前端在onOpen中立即发送消息将token传递到后端,后端获取到token消息后对其进行鉴权并记录,以后每次消息交互时都要判断一下鉴权记录是否通过。(这样做是为了保证建立成功但未鉴权的链接不能正常收发消息)
  3. 通过子协议传递鉴权信息,由于js的websocket不能设置header信息,但是可以通过设置子协议(new WebSocket(url [, protocols]);),而子协议实际上是放到header对应的Sec-WebSocket-Protocol中的

由于基础注解的方式对握手请求封装的比较深,后端获取token的方式比较特殊,这里只演示第三种获取方式。可以使用过滤器获取请求信息,也可以使用拦截器,这里使用过滤器演示。

@Slf4j
@Order(1)
@WebFilter(filterName = "websocketFilter", urlPatterns = "/webSocket/*")
public class WebsocketFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletResponse resp = (HttpServletResponse) response;
        HttpServletRequest req = (HttpServletRequest) request;
        String token = req.getHeader(HandshakeRequest.SEC_WEBSOCKET_PROTOCOL);
        log.info("WS:HEADER-TOKEN:{}", token);
        // 权限校验
        String checkResult = checkToken(token);
        if (!StringUtils.isEmpty(checkResult)) {
            resp.getWriter().write("WS权限校验失败:" + checkResult);
            resp.getWriter().flush();
            log.error("WS:鉴权失败:{}", checkResult);
            return;
        }
        // response中也要将token信息返回,否则连接建立失败
        resp.setHeader(HandshakeRequest.SEC_WEBSOCKET_PROTOCOL, token);
        chain.doFilter(request, response);
    }

    /**
     * 校验token
     * @param token
     * @return
     */
    private String checkToken(String token) {
        if (StringUtils.isEmpty(token)) {
            return "缺少TOKEN信息";
        }
        // 在这里做token校验
    }
}

由于使用了@WebFilter,需要在程序的入口类(**Application.java)中引入@ServletComponentScan注解,否则WebFilter不会生效。

至此后端websokcet服务搭建完毕。

Nginx配置

通常后端服务不会直接对外开放,而是通过nginx做代理,这时需要在nginx的location中添加如下两行配置,这样ws/wss协议才能正常访问。

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";

前端部分

前端部分比较简单需要注意的地方是鉴权部分token的传递方式

var websocket = new WebSocket(url, [token]);
websocket.onopen = function() {
    console.log("数据发送中...");
};

websocket.onmessage = function (evt) {
    var received_msg = evt.data;
    console.log("接收数据..." + received_msg);
};

websocket.onclose = function(evt) {
    // 关闭
    console.log("连接已关闭...");
    console.log(evt);
};

websocket.onerror = function(evt) {
    // 异常
    console.log("异常: ");
    console.log(evt);
};

心跳检测

如果后端服务是通过nginx做的代理,那么nginx配置的proxy_read_timeout(默认是1分钟)会主动断开连接,解决此问题有2种方式,一种是配置更长的超时时间,但是长时间的等待会影响服务端的性能。不建议使用此种方式。第二种是前端增加心跳请求,心跳间隔需要小于nginx的超时时间。在链接建立(onopen)后开启心跳

/**
 * 心跳检测
 * @param websocket 
 * @param timeout 心跳间隔(单位毫秒)
 */
function HeartCheck(websocket, timeout) {
    this.timeout = timeout;
    this.websocket = websocket;
    this.intervalObj = null;
    // 清除心跳
    this.clear = function () {
        clearInterval(this.intervalObj);
    },
    this.start = function() {
        var self = this;
        this.intervalObj = setInterval(function() {
            // 这里发送一个心跳,后端收到后,返回一个心跳消息
            self.websocket.send("ping");
            console.log("ping")
        }, this.timeout)
    }
}

断线重连

某些情况下不可避免造成请求中断,如服务器重启,为了保证前端正常使用,需要引入断线重连机制,只需要在链接关闭时调用此函数即可

// 重新连接
function reconnect(url, obj) {
    // 这里设置延迟时间是为了避免请求过于频繁,可以适当设置
    setTimeout(function () {
        initWebSocket(url, obj);
    }, 2000);
}

完整的js代码

/**
 * 调用websocket(带有失败重连及心跳机制)
 * @param url       ws/wss协议的请求链接
 * @param obj       请求数据,会传递给onOpen函数
 * @param onOpen    链接建立成功后调用此函数
 * @param onMessage 接收到后端消息后调用此函数
 * @param onError   出现异常调用此函数
 * @param onClose   关闭websocket会调用次函数
 */
 function initWebSocket(url, obj, onOpen, onMessage, onError, onClose) {
    var data = {
        user: obj.user,
        token: obj.token
    }

    console.log("socket链接: " + url);
    if('WebSocket' in window) {
        // 通过子协议实现鉴权
        var websocket = new WebSocket(url, [data.token]);
        // 创建心跳对象
        var heartCheck = new HeartCheck(websocket, 30000); // 设置间隔30秒的心跳
        
        websocket.onopen = function() {
            // 开启心跳
            heartCheck.start();
            websocket.send("发送数据");
            console.log("数据发送中...");
            if (onOpen) {
                onOpen(data)
            }
        };

        websocket.onmessage = function (evt) {
            var received_msg = evt.data;
            console.log("接收数据..." + received_msg);
            // 不是心跳消息,则处理此消息
            if(received_msg != 'pong') {
                if (onMessage) {
                    onMessage(received_msg)
                }
            }
        };

        websocket.onclose = function(evt) {
            // 关闭
            console.log("连接已关闭...");
            console.log(evt);
            console.log("重新连接...");
            if (onClose) {
                onClose(evt)
            }
            heartCheck.clear();
            reconnect(url, obj, onOpen, onMessage, onError, onClose);
        };

        websocket.onerror = function(evt) {
            // 异常
            console.log("异常: ");
            console.log(evt);
            if (onError) {
                onError(evt);
            }
            websocket.close();
        };

        // 监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
        window.onbeforeunload = function() {
            websocket.close();
        }
    } else {
        alert("浏览器不支持websocket");
    }
}

// 重新连接
function reconnect(url, obj, onOpen, onMessage, onError, onClose) {
    // 这里设置延迟时间是为了避免请求过于频繁,可以适当设置
    setTimeout(function () {
        initWebSocket(url, obj, onOpen, onMessage, onError, onClose);
    }, 2000);
}

/**
 * 心跳检测
 * @param websocket 
 * @param timeout 心跳间隔(单位毫秒)
 */
function HeartCheck(websocket, timeout) {
    this.timeout = timeout;
    this.websocket = websocket;
    this.intervalObj = null;
    // 清除心跳
    this.clear = function () {
        clearInterval(this.intervalObj);
    },
    this.start = function() {
        var self = this;
        this.intervalObj = setInterval(function() {
            // 这里发送一个心跳,后端收到后,返回一个心跳消息
            self.websocket.send("ping");
            console.log("ping")
        }, this.timeout)
    }
}

调用

// 登录成功后建立ws
function loginSuccess() {
    var user = $("[name=user]").val();
    var token = $("[name=token]").val();
    var data = {
        user: user,
        token: token
    }
    var protocol = window.location.protocol;
    if (protocol === "https:") {
        protocol = "wss:";
    } else {
        protocol = "ws:";
    }
    var host = window.location.host;
    var url = protocol + "//" + host + "/webSocket/" + data.user;

    initWebSocket(url, data, 
    (data) => {
        // do onOpen
    }, (message)=>{
        // do onMessage
    });
}

相关文章

  • 基于springboot的websocket完整示例(包括鉴权,

    本篇文章从后端到前端完整的展示springboot基于注解的方式使用websocket,鉴权及前端js代码示例(心...

  • 谈谈鉴权与授权

    目录 鉴权场景实现 授权场景实现 鉴权 鉴权(authentication): 你是谁 场景 实现 关于鉴权的示例...

  • 基于SpringBoot、STOMP使用WebSocket实现聊

    原文连接: 基于SpringBoot、STOMP使用WebSocket实现聊天室功能 WebSocket: 新项...

  • WebSocket连接鉴权的过程

    鉴权授权方案 根据WebSocket文档上的说明,鉴权授权是需要自己实现。我们自己实现的流程大概是,在每次连接前,...

  • JWT

    概述 JWT 基于 token 的鉴权机制,基于 token 的鉴权机制类似于 http 协议也是无状态的,它不需...

  • SpringBoot-Websocket示例

    socket-demo WebSocket示例 工作中有这样一个需示,我们把项目中用到代码缓存到前端浏览器Inde...

  • websocket 简单搭建

    注意:本文WebSocket是基于SpringBoot框架注解使用 pom文件依赖 application.yml...

  • 学习mall电商系统【开源】

    完整的电商系统。包括前台商城系统及后台管理系统。【开源】 基于SpringBoot+MyBatis实现。 前台商城...

  • github干货

    github 圆形头像 模仿微信放大缩小交互 基于springboot的Websocket实现单聊群聊

  • SpringBoot快速实现鉴权系统,基于LoopAuth

    LoopAuth一款低侵入、精简、轻量、细粒度的Java Web权限管理框架 目前包含如下功能: 注解鉴权 代码鉴...

网友评论

      本文标题:基于springboot的websocket完整示例(包括鉴权,

      本文链接:https://www.haomeiwen.com/subject/kmaphrtx.html