日常记录:springboot 给websocket 添加拦截、认证(二)

前言

上一章的websocket拦截和这次讲的不同,这次是stomp协议广播消息的拦截。

一、配置拦截

这次是由AbstractWebSocketMessageBrokerConfigurer抽象类里面一个configureClientInboundChannel方法,添加拦截器。

自定义WebSocketEmptyLogInterceptor拦截器,覆盖ChannelInterceptorAdapter接口preSend方法。ChannelInterceptorAdapter是ChannelInterceptor的适配器,覆盖preSend,在发送前认证token即可。

package com.sakyoka.test.systemconfig.interceptor;

import org.apache.commons.lang3.StringUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptorAdapter;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.stereotype.Component;

import com.sakyoka.test.utils.JwtTokenUtils;

import lombok.extern.log4j.Log4j;

/**
 * 
 * 描述:stomp websocket 拦截器
 * @author sakyoka
 * @date 2022年9月2日 下午5:27:12
 */
@Component
@Log4j
public class WebSocketEmptyLogInterceptor extends ChannelInterceptorAdapter{

    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
    	final StompHeaderAccessor accessor = 
    			MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
    	//连接
    	if (StompCommand.CONNECT.equals(accessor.getCommand())) {
        	String token = accessor.getFirstNativeHeader("token");
    		if (StringUtils.isBlank(token)){
    			log.info("token is null");
    			throw new RuntimeException("token is null");
    		}
    		
    		//判断token有效性
    		boolean tokenValid = JwtTokenUtils.tokenValid(token);
    		if (!tokenValid){
    			log.info("invalid token");
    			throw new RuntimeException("invalid token");
    		}
    	}
    	return message;
    }
}

在第一次连接时候,从头部信息获取token值,这里是从header获取,对应前端需要传到header。

注册WebSocketEmptyLogInterceptor拦截器,在configureClientInboundChannel的setInterceptors添加

package com.sakyoka.test.systemconfig.socket;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

import com.sakyoka.test.systemconfig.interceptor.WebSocketEmptyLogInterceptor;
import com.sakyoka.test.systemconfig.interceptor.WebSocketReadLogInterceptor;
import com.sakyoka.test.webscoketlog.websocket.TokenWebSocketHandler;

/**
 * 
 * 描述:开启WebSocket、注册ServerEndpointExporter实例、开启STOMP协议来传输基于代理
 * @author sakyoka
 * @date 2022年8月14日 2022
 */
@Configuration
@EnableWebSocket
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer implements WebSocketConfigurer{

	@Autowired
	WebSocketReadLogInterceptor webSocketReadLogInterceptor;
	
	@Autowired
	WebSocketEmptyLogInterceptor webSocketEmptyLogInterceptor;
	
	@Autowired
	TokenWebSocketHandler tokenWebSocketHandler;
	
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
    
	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		//添加一个stomp协议的endpoint
		registry.addEndpoint("/server").withSockJS();
	}
	
	@Override
	public void configureMessageBroker(MessageBrokerRegistry registry) {
		//添加一个topic代理节点
		registry.enableSimpleBroker("/topic");
	}
	
	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
		
		// /ws/websocket/log 路径就是前端要访问的路径 类似@ServerEndpoint("/websocket/log")
		//添加处理器、添加拦截地址、添加拦截器
		registry.addHandler(tokenWebSocketHandler, "/ws/websocket/log")
		        .setAllowedOrigins("*")
		        .addInterceptors(webSocketReadLogInterceptor);
		
		/* use configureClientInboundChannel interceptors
		 * 放弃addHandler方式, 目前测试这种使广播订阅地址失效 
		*/
//		registry.addHandler(new TextSocketHandler(), "/server/**")
//				.addInterceptors(webSocketInterceptor)
//				.setAllowedOrigins("*")  
//		        .withSockJS();
	}
	
	@Override
	public void configureClientInboundChannel(ChannelRegistration registration) {
		//添加路由拦截,判断请求头是否带上token、token是否有效
		registration.setInterceptors(webSocketEmptyLogInterceptor);
	}
}

二、页面添加header

页面代码

<%@ page contentType="text/html; charset=UTF-8" %>
<!DOCTYPE html>
<html>
<head> 
    <title>jarLog</title>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
	<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
	<meta name="renderer" content="webkit">
    <jsp:include page="/WEB-INF/views/common/commonstatic.jsp" flush="true" />
    <!-- socketjs插件 -->
    <script type="text/javascript" src="${root}/components/socketjs/sockjs.js"></script>
    <script type="text/javascript" src="${root}/components/socketjs/stomp.js"></script>
    <!-- jquery菜单 -->
    <link rel="stylesheet" type="text/css" href="${root}/components/jquery/jquery.contextMenu.css"/>
    <script type="text/javascript" src="${root}/components/jquery/jquery.contextMenu.js"></script>
</head>
<body>
<div style="background-color:black;width:99%; height:500px; padding: 10px" id="console-parent">
    <div id="console" style="width:99%; height:95%; color:white;overflow-y: auto; overflow-x:hidden;"></div>
</div>
</body>
<script type="text/javascript" src="${root}/js/console.js"></script>
<script type="text/javascript" src="${root}/js/console-websocket.js"></script>
<script type="text/javascript" src="${root}/js/console-websocket-stomp.js"></script>
<script type="text/javascript">
//已在登录时候设置到缓存里面
var token = localStorage.getItem("token");
//var port = window.location.port;//如果经过代理?这个经过网关是网关的端口
//var port = "${pageContext.request.serverPort}";//这个才是后端端口
var jarWebSocket;
var jarConsole;
//jarId根据实际定义传过来,现在只有本系统的日志打印可以定义为system,其它jar的可以定义一个uuid关联标识
var jarId = "system";
//@ServerEndpoint("/log")
var wsurl = 'ws://'+ ip +':'+ port + root +'/ws/websocket/log?jarId=' + jarId + "&token=" token;
//registry.addEndpoint("/server").withSockJS();
var serverurl = 'http://'+ ip +':'+ port + root + '/server';
$(function(){
	//添加console-parent内容变化,调整滚动条位置,自动滚动最下面
	$("#console").bind("DOMNodeInserted",function(e){
	 	var height = $(this).prop("scrollHeight");
	 	$(this).animate({scrollTop: height}, 10);		
	});
	
	registerConsoleLogSocket();
	
	registerEmptyLogSocket();
	
	addRightClickListener();
});

/**
 * 注册控制台打印的socket事件
 */
function registerConsoleLogSocket(){
	jarConsole = new JarConsole();
	jarConsole.load('console');
    jarWebSocket= new JarWebSocket({
		url: wsurl,
		//获取后台返回信息
		onmessage: function(event){
			jarConsole.fill(event.data);
		}
	
	}).addEventListener();
}

/**
 * 注册清空日志的socket事件,采用消息订阅形式
 */
function registerEmptyLogSocket(){
	new StompSocketDefine({
		serverUrl: serverurl,
		//这里添加token
		headers: {token: token},
		subscribes: [{
			subscribeUrl: '/topic/emptylog',
			onmessage: function(res){
				//后端通知需要重新连接
				if (res.body == jarId && jarWebSocket.isConnect()){
					jarWebSocket.close();
					jarWebSocket.reset().reconnect();	
				}
			}
		}]
	}).connect();
}

/**
 * 添加右键菜单
 */
function addRightClickListener(){
	var items = {
	         "clear": {
	        	 name: "清空控制台信息", 
	        	 callback: function(){
	        		 jarConsole.clear();
	          }},
	         'emptyLog': {
	        	 name: "清空日志文件内容",
	        	 callback: function(){
	        		 var url = root + '/log/emptylog';
	        		 $.get(url, {jarId: jarId}, function(res){
	        			 console.log("emptylog result >>> " + res);
	        		 });
	        	 }
	         }
    };
	
	//右键菜单
    $.contextMenu({
        selector: '#console', 
        events:{preShow: function(){}},
        items: items
    });	
}
</script>
</html>	

在registerEmptyLogSocket的订阅方法参数上添加headers,追加token值。

对应改造console-websocket-stomp.js,添加headers传值

/**
 * add by sakyoka 2022-08-26
 * stomp协议的scoket
 * 
 */
var StompSocketDefine = function(config){
	
	/**配置数据对象 */
	config = config || {};
	/**server 地址*/
	var serverUrl = config.serverUrl;
	/**订阅信息集合*/
	var subscribes = config.subscribes || [];
	/**创建socket*/
	var socket = new SockJS(serverUrl);
	/**使用stomp协议*/
	var stompClient = Stomp.over(socket);
	/**是否自动重连 */
	var autoConnect = config.autoConnect || true;
	/**判断是否主动关闭*/
	var driving = false; 
	/** 尝试重新连接次数*/
	var defaultFailTryConnTimes = config.failTryConnTimes || 5;
	/**尝试累加次数 */
	var tryConnTimes = 0;
	/**是否连接中 */
	var isConnect = false;
	var _this = this;
	/** headers对象*/
	var headers = config.headers || {};
	
	/**
	 * 恢复初始状态值
	 */
	this.reset = function(){
		autoConnect = config.autoConnect || true;
		driving = false; 
		defaultFailTryConnTimes = config.failTryConnTimes || 5;
		tryConnTimes = 0;
		isConnect = false;
	}
	
	/**
	 * 获取client
	 */
	this.getStompClient = function(){
		return stompClient;	
	}
	
	/**
	 * 断开连接
	 */
	this.disconnect = function(){
		driving = true;
		if (stompClient != undefined){
			stompClient.disconnect();
		}
		return this;
	}
	
	/**
	 * 连接
	 */
	this.connect = function(){
		
		/**
		 * 链接服务
		 */
		stompClient.connect(headers, function(frame){
			isConnect = true;
			driving = false;
			tryConnTimes = 0;
			console.log("stomp socket link state: "+ frame);
			/**
			 * 遍历订阅地址
			 */
			for (var i in subscribes){
				var subscribeObject = subscribes[i];
				var subscribeUrl = subscribeObject.subscribeUrl;
				var onmessage = subscribeObject.onmessage;
				stompClient.subscribe(subscribeUrl, function(res){
					if (onmessage){
						onmessage(res);
					}
				});
			}
			
		}, function(){	
			isConnect = false;
			console.log('stomp socket link error');
			if (isConnect === false 
			         && autoConnect === true 
			         && driving === false){
				if (defaultFailTryConnTimes > tryConnTimes){
					_this.tryToConnect();
				}
			}
		});	
		
		return this;
	}
	
	/**
	 * 尝试连接
	 */
	this.tryToConnect = function(){
		console.log('stomp socket try to connect...');
		socket = new SockJS(serverUrl);
		stompClient = Stomp.over(socket);
	    var _tryToConnect = function (){
		
			tryConnTimes += 1;
			if (defaultFailTryConnTimes < tryConnTimes){
				console.log('stomp socket try to connect times is more than max times:' + tryConnTimes);
				return ;
			}
			
			if (isConnect === true){
				return ;
			}
			
			_this.connect();
			
			setTimeout(function(){
				_tryToConnect();
		    }, 3000);
		}
		
		_tryToConnect();
	}
	
	/**
	 * 刷新页面前把连接关掉
	 */
	window.onbeforeunload = function(){
		
		driving = true;
		
		if (stompClient != undefined){
			stompClient.disconnect();
		}
	}
}  

ok,测试

三、测试

带token,可以发现有传token,连接正常

 不带token,发现有异常信息,连接失败

 ok,拦截成功了。

附件

​​​​​​​springboot+websocket消息广播接口拦截-Java文档类资源-CSDN下载

拓展

springboot集成websocket 实时输出日志到浏览器(一)_sakyoka的博客-CSDN博客_websocket实时显示日志

springboot集成websocket 清空日志后消息广播通知前端重新连接(二)_sakyoka的博客-CSDN博客

springboot 集成JWT 登录验证_sakyoka的博客-CSDN博客

日常记录:springboot 给websocket 添加拦截、认证(一)_sakyoka的博客-CSDN博客

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
以下是在Spring Boot项目中整合WebSocket的步骤,包括拦截器、消息处理器和WebSocket实现类的编写: 1. 添加依赖 在pom.xml文件中添加以下依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> ``` 2. 编写WebSocket配置类 创建一个WebSocketConfig类,用于配置WebSocket相关的内容,包括注册WebSocket处理器、拦截器等。 ```java @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Autowired private MyWebSocketHandler myWebSocketHandler; @Autowired private MyHandshakeInterceptor myHandshakeInterceptor; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myWebSocketHandler, "/websocket") .addInterceptors(myHandshakeInterceptor); } } ``` 在这个配置类中,我们注册了一个WebSocket处理器和一个握手拦截器。WebSocket处理器用于处理WebSocket连接和消息,握手拦截器用于拦截WebSocket连接请求并进行一些处理。 3. 编写WebSocket处理器 创建一个WebSocket处理器类,用于处理WebSocket连接和消息。 ```java @Component public class MyWebSocketHandler extends TextWebSocketHandler { private static final Logger logger = LoggerFactory.getLogger(MyWebSocketHandler.class); private static final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { logger.info("WebSocket连接建立成功:{}", session.getId()); sessions.put(session.getId(), session); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { logger.info("收到消息:{}", message.getPayload()); // 处理消息 } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { logger.info("WebSocket连接关闭:{},状态:{}", session.getId(), status); sessions.remove(session.getId()); } public static void sendMessage(String sessionId, String message) throws IOException { WebSocketSession session = sessions.get(sessionId); if (session != null && session.isOpen()) { session.sendMessage(new TextMessage(message)); } } } ``` 在这个处理器类中,我们重写了WebSocket处理器的三个方法:afterConnectionEstablished、handleTextMessage和afterConnectionClosed。afterConnectionEstablished方法在WebSocket连接建立成功后被调用,handleTextMessage方法用于处理收到的消息,afterConnectionClosed方法在WebSocket连接关闭后被调用。 4. 编写握手拦截器 创建一个握手拦截器类,用于拦截WebSocket连接请求并进行一些处理。 ```java @Component public class MyHandshakeInterceptor implements HandshakeInterceptor { private static final Logger logger = LoggerFactory.getLogger(MyHandshakeInterceptor.class); @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { logger.info("WebSocket握手拦截器:beforeHandshake"); // 进行一些处理 return true; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { logger.info("WebSocket握手拦截器:afterHandshake"); } } ``` 在这个握手拦截器类中,我们重写了握手拦截器的两个方法:beforeHandshake和afterHandshake。beforeHandshake方法在WebSocket连接请求到达服务器端时被调用,afterHandshake方法在WebSocket连接握手成功后被调用。 5. 编写WebSocket实现类 创建一个WebSocket实现类,用于实现WebSocket的具体业务逻辑。 ```java @Controller public class MyWebSocketController { @MessageMapping("/hello") @SendTo("/topic/greetings") public String greeting(String message) throws Exception { Thread.sleep(1000); // 模拟处理时间 return "Hello, " + message + "!"; } } ``` 在这个WebSocket实现类中,我们使用了@MessageMapping注解来指定处理消息的路径,使用@SendTo注解来指定返回消息的路径。 6. 测试WebSocket连接 在客户端中,可以使用JavaScript代码来测试WebSocket连接: ```javascript var socket = new WebSocket("ws://localhost:8080/websocket"); socket.onopen = function(event) { console.log("WebSocket连接已建立"); socket.send("Hello, WebSocket!"); }; socket.onmessage = function(event) { console.log("收到消息:" + event.data); }; socket.onclose = function(event) { console.log("WebSocket连接已关闭"); }; ``` 以上就是在Spring Boot项目中整合WebSocket的步骤,包括拦截器、消息处理器和WebSocket实现类的编写。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值