SpringBoot+Vue+WebSocket+Token验证

第一步引入WebSocket依赖

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

第二步配置WebSocket

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
@EnableWebSocket
public class WebSocketConfig  {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

第三步实现自己的服务器逻辑


import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

// 日志打印 没有 就用sout
@ServerEndpoint(value = "/webSocket/{username}") //相对于接口名 @RequestMapping("/webSocket/{username}")
@Component
public class WebSocketServiceImpl {
    private static final Logger LOG = LoggerFactory.getLogger(WebSocketServiceImpl.class);
    // 线程安全的map
    private static Map<String, WebSocketServiceImpl> webSocketMap = new ConcurrentHashMap<>();
	// 原子 int
    private static AtomicInteger connetCout = new AtomicInteger(0);
    // getter setter
    private Session session;

    private String username;

    private static synchronized Map<String, WebSocketServiceImpl> getWebSocket() {
        return webSocketMap;
    }
	
	/**
     * 初始化时执行
     */
    @PostConstruct
    public void init() {
        LOG.debug("===websocket init===");
    }

	/**
	 * 传入 username 以区分不同会话
     * 建立会话时执行
     */
    @OnOpen
    public void onOpen(@PathParam("username") String username, Session session) {
        this.username = username;
        this.session = session;
        webSocketMap.put(username, this);
        connetCout.incrementAndGet();
        sendMessage("hhh", username);
        LOG.debug("==========={} Session 连接成功============", username);
        LOG.debug("Session ID :{}", session.getId());
        LOG.debug("=====size {}==========", webSocketMap.size());
    }
	
	/**
     * 收到客户端消息时执行
     *
     * @param msg
     * @param session
     */
    @OnMessage
    public void onMessage(String msg, Session session) {
    	//自己实现 收到客户端消息之后的处理逻辑
    	//给客户端返回消息 或者 操作数据库等等一系列操作
        LOG.debug("收到消息:{}", msg);
        sendMessage("你发的消息是:" + msg, username);
    }

    @OnClose
    public void onClose() {
        LOG.debug("===============close{}", username);
        webSocketMap.remove(username);
        if (session != null) {
            try {
                session.close();
                connetCout.decrementAndGet();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    @OnError
    public void onError(Session session, Throwable error) {
        error.printStackTrace();
        LOG.debug("用户名{}", this.username);
        LOG.debug("会话是否连接{}", session.isOpen());
        LOG.debug("会话id{}", session.getId());
    }

    /**
     * 发送消息给指定人
     *
     * @param msg      消息
     * @param receiver 消息接收方
     */
    public static void sendMessage(String msg, String receiver) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            String json_msg = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(msg);
            WebSocketServiceImpl webSocketService = webSocketMap.get(receiver);
            if (webSocketService != null) {
                Session session = webSocketService.session;
                if (session.isOpen()) {
                    session.getAsyncRemote().sendText(json_msg);
                }
            }
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
    }
    
    
    public static void sendMessage(String msg, String[] receivers) {
        for (String receiver : receivers) {
            sendMessage(msg, receiver);
        }
    }
	//广播消息 略

    public int getSize() {
        return webSocketMap.size();
    }
}

第四步Vue实现客户端

用的是element-UI

<template>
  <el-form :model="data">
    <el-form-item label="消息">
      <el-input v-model="data.msg"></el-input>
    </el-form-item>
    <el-form-item>
   	  <el-button @click="init">
        初始化
      </el-button>
      <el-button @click="doSubmit">
        发送
      </el-button>
    </el-form-item>
  </el-form>
</template>

<script>
export default {
  data() {
    return {
      data: {
        msg: ""
      },
      //@ServerEndpoint(value = "/webSocket/{username}")
      path: "ws://localhost:8866/webSocket/", //  ws:// +ip:port/webSocket/
    }
  },
  mounted() {
    // this.init();
  },
  methods: {
    doSubmit() {
      console.log(this.data.msg)
      this.send(this.data.msg)
    },
    init: function () {
      if (typeof (WebSocket) === "undefined") {
        this.$message.error("您的浏览器不支持socket")
      } else {
        // 实例化socket 此处 是登录时存放的 token 和 username
        let token = sessionStorage.getItem("token")
        let username = sessionStorage.getItem("username")
        // 拼接 path : ws://localhost:8866/webSocket/username
        // 如果不需要token验证 则写成 
        this.socket = new WebSocket(this.path + username)
        // 带token 因为ws协议无法编辑请求头 这里通过子协议携带token
       // this.socket = new WebSocket(this.path + username, [token])
        // 监听socket连接
        this.socket.onopen = this.open
        // 监听socket错误信息
        this.socket.onerror = this.error
        // 监听socket消息
        this.socket.onmessage = this.getMessage
      }
    },
    open: function () {
      console.log("socket连接成功")
    },
    error: function () {
      console.log("连接错误")
      console.log(arguments)
    },
    getMessage: function (msg) {
      console.log(msg.data)
    },
    // 发送消息给被连接的服务端
    send: function (params) {
      this.socket.send(params)
    },
    close: function () {
      console.log("socket已经关闭")
    },

  },
  destroyed() {
    this.socket.onclose = this.close
  }
};
</script>


<style scoped>

</style>
  1. 连接服务器
    在这里插入图片描述

客户端浏览器控制台打印
在这里插入图片描述
2. 服务器打印(不要纠结会话id,已经是第N次连接了)
在这里插入图片描述
3. 发消息
在这里插入图片描述
在这里插入图片描述

到此,一个简单的案例已经实现了

我项目里还用了Spring-Security

因为项目一开始使用了token,上面的ws协议并不能像http一样携带token,我在引入token时,客户端一连接就立即断开
在这里插入图片描述
在这里插入图片描述

下面开始解决token问题
在这里插入图片描述


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

import javax.annotation.PostConstruct;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;


@Component
public class WebSocketFilter implements Filter {
    public static final Logger LOG = LoggerFactory.getLogger(WebSocketFilter.class);

    private static final List<String> PATTERN_LIST = new ArrayList<>();

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        LOG.debug("==============doFilter==============");
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        String servletPath = request.getServletPath();
        
        if (matcher(servletPath)) {
        	//自己定义的静态变量 实际就是"Sec-Websocket-Protocol"
            //HandshakeRequest.SEC_WEBSOCKET_PROTOCOL ws包里有一个一样的
            String token = request.getHeader(HeadersConstants.SEC_WEBSOCKET_PROTOCOL); 
            
            //这里CommonService.checkToken 是自己验证token的逻辑 验证通过就返回true
            //比如前面vue [token]中的token="asdfghj"
            //这里 boolean check = "asdfghj".equals(token)  返回true
            // 怎么校验自己定
            boolean check = CommonService.checkToken(token, response);
            if (check) {
                // 这里 我看网上人家是没有这一步的 但是我不设置就会连接成功就断开
                response.setHeader(HeadersConstants.SEC_WEBSOCKET_PROTOCOL, token);
                // if pass 没有这一步就被拦截了
                chain.doFilter(servletRequest, servletResponse);
            }
        } else {
            chain.doFilter(servletRequest, servletResponse);
        }
    }

    @PostConstruct
    public void init() {
        //初始化时注入 后续过滤项多了可以配置批量注入
        PATTERN_LIST.add("/webSocket/**");
    }

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

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

    public boolean matcher(String servletPath) {
        AntPathMatcher matcher = new AntPathMatcher(File.separator);
        return PATTERN_LIST.stream().anyMatch(pattern -> matcher.match(pattern, servletPath));
    }
}

这里最主要的就是在response添加 这个

response.setHeader(HeadersConstants.SEC_WEBSOCKET_PROTOCOL, token);

解决完之后 vue 写成

 let token = '你的token'
 this.socket = new WebSocket(this.path + username, [token])

没加token之前Request Headers是没有这个的

在这里插入图片描述
在过滤器中设置的Sec-Websocket-Protocol也成功反回来了
Response Headers
在这里插入图片描述
最后,引入token验证之后就连接失败这个问题,卡了我两天,期间尝试了各种方式拦截token都没有成功。最终发现少了这一行代码解决response.setHeader

首先,需要明确的是,WebSocket 是一种基于 TCP 协议的双向通信协议,在 WebSocket 连接建立后,服务器和客户端可以相互发送消息,而不需要像 HTTP 那样通过请求和响应交换数据。 在使用 WebSocket 校验 Token 时,可以采用以下步骤: 1. 客户端在连接 WebSocket 时,将 Token 作为请求头中的一个字段进行传递。 2. 服务器在接收到 WebSocket 连接请求时,从请求头中获取 Token,并对 Token 进行校验。 3. 如果 Token 校验通过,则将 WebSocket 连接建立成功,并返回一个成功的响应;如果 Token 校验失败,则拒绝建立 WebSocket 连接,并返回一个错误的响应。 以下是一个示例代码,用于在 Node.js 服务器上实现基于 WebSocketToken 校验: ```javascript const WebSocket = require('ws'); const jwt = require('jsonwebtoken'); const wss = new WebSocket.Server({ port: 8080 }); wss.on('connection', (ws, req) => { const token = req.headers['authorization']; try { const decoded = jwt.verify(token, 'secret'); // Token 校验通过,建立 WebSocket 连接 console.log('WebSocket connection established'); ws.on('message', (message) => { console.log(`Received message: ${message}`); }); ws.send('Connection established'); } catch (err) { // Token 校验失败,拒绝建立 WebSocket 连接 console.error('WebSocket connection rejected:', err.message); ws.close(); } }); ``` 在上面的示例中,我们使用了 `jsonwebtoken` 库来对 Token 进行解析和校验。在实际应用中,需要根据具体的业务场景和安全要求,选择合适的 Token 校验库和方式。同时,也需要注意在传递 Token 时,要使用 HTTPS 等安全的传输协议,以保障 Token 的安全性。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值