前言
上一章的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文档类资源-优快云下载
拓展
springboot集成websocket 实时输出日志到浏览器(一)_sakyoka的博客-优快云博客_websocket实时显示日志
springboot集成websocket 清空日志后消息广播通知前端重新连接(二)_sakyoka的博客-优快云博客