前言
嗯。这用jwt做认证校验时候,拦截器居然拦截不到@ServerEndpoint的地址,神奇了。
然后用了另外一种方式拦截。。
一、最初处理方式
之前是有这样一个类,读取日志信息
package com.sakyoka.test.webscoketlog.websocket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import org.springframework.stereotype.Component;
import com.sakyoka.test.utils.Command;
import lombok.extern.log4j.Log4j;
/**
*
* 描述:读取日志信息
* @author sakyoka
* @date 2022年8月14日 上午11:01:14
*/
@ServerEndpoint("/websocket/log")
@Log4j
@Component
public class WebSocketLog {
private Process process;
private InputStream inputStream;
private String jarId;
private static final ExecutorService EXECUTOR_SERVICE = Executors.newCachedThreadPool();
public static final String SYSTEM_FLAG = "system";
@OnOpen
public void onOpen(Session session) {
Map<String, List<String>> params = session.getRequestParameterMap();
//暂时没有用处,但是如果用jarId关联每一个jar可以找到对应的日志文件,目前用system为例
jarId = params.get("jarId").get(0);
String logPath = null;
if (SYSTEM_FLAG.equals(jarId)) {
//logging.file=D:\\system.log
logPath = "D:\\system.log";
}else {
if (params.containsKey("logPath")){
logPath = params.get("logPath").get(0);
}
//或 根据jarId 获取日志文件路径
}
//window系统 tail命令需要添加tail.exe小工具到system32
String cmd = "tail -f " +logPath;
log.debug(String.format("show log cmd >> %s", cmd));
Command command = Command. getBuilder().commandStr(cmd).autoReadStream(false);
command.exec();
process = command.getProcess();
inputStream = process.getInputStream();
EXECUTOR_SERVICE.execute(() -> {
String line;
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(inputStream));
while((line = reader.readLine()) != null) {
session.getBasicRemote().sendText(line + "<br>");
}
} catch (IOException e) {
}
});
}
@OnMessage
public void onMessage(String message, Session session){
log.debug(String.format("socket onmessage ==> 接收到信息:%s", message));
}
@OnClose
public void onClose(Session session) {
this.close();
log.debug(String.format("socket已关闭"));
}
@OnError
public void onError(Throwable thr) {
this.close();
log.debug(String.format("socket异常,errorMessage:%s" , thr.getMessage()));
}
private void close(){
//这里应该先停止命令, 然后再关闭流
if(process != null){
process.destroy();
}
try {
if (Objects.nonNull(inputStream)){
inputStream.close();
}
} catch (Exception e) {}
}
}
CommonInterceptor拦截器
package com.sakyoka.test.systemconfig.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import com.sakyoka.test.utils.JwtTokenUtils;
import lombok.extern.log4j.Log4j;
/**
*
* 描述:请求拦截器
* @author sakyoka
* @date 2022年9月2日 下午1:54:41
*/
@Component
@Log4j
public class CommonInterceptor implements HandlerInterceptor{
private static final int UNAUTHORIZED = 401;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
//从header中获取token值
String token = request.getHeader("token");
token = StringUtils.isBlank(token) ? request.getParameter("token") : token;
if (StringUtils.isBlank(token)){
log.debug("header token is null");
response.setStatus(UNAUTHORIZED);
response.sendRedirect("/login");
return false;
}
//判断token有效性
boolean tokenValid = JwtTokenUtils.tokenValid(token);
if (!tokenValid){
log.debug("invalid token");
response.sendRedirect("/login");
response.setStatus(UNAUTHORIZED);
return false;
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)throws Exception {
}
}
注册拦截器
package com.sakyoka.test.systemconfig.web;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.sakyoka.test.systemconfig.interceptor.CommonInterceptor;
/**
*
* 描述:web配置
* @author sakyoka
* @date 2022年9月2日 下午1:59:04
*/
@Configuration
public class WebConfig extends WebMvcConfigurerAdapter{
@Autowired
CommonInterceptor commonInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截所有/log 开头接口
registry.addInterceptor(commonInterceptor)
.addPathPatterns("/log/**", "/websocket/log");
}
}
发现不携带token,/websocket/log 还是能正常访问。
查阅资料还有另外一种处理,实现WebSocketConfigurer接口,在registerWebSocketHandlers方法里面添加处理器、拦截器、拦截地址
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.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.handler.TextWebSocketHandler;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
import com.sakyoka.test.systemconfig.interceptor.WebSocketReadLogInterceptor;
/**
*
* 描述:开启WebSocket、注册ServerEndpointExporter实例、开启STOMP协议来传输基于代理
* @author sakyoka
* @date 2022年8月14日 2022
*/
@Configuration
@EnableWebSocket
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer implements WebSocketConfigurer{
@Autowired
WebSocketReadLogInterceptor webSocketReadLogInterceptor;
@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) {
registry.addHandler(new TextWebSocketHandler(), "/websocket/log")
.setAllowedOrigins("*")
.addInterceptors(webSocketReadLogInterceptor);
}
}
然而,在不携带token情况下还是能正常访问。
或者自定义一个WebSocketHandler,发现里面的方法没有输出信息。
但是在注释WebSocketLog的@Component之后,发现拦截成功了,而且自定义的WebSocketHandler对应方法也有信息了。。这里猜测是@ServerEndpoint覆盖了egistry.addHandler(new TextWebSocketHandler(), "/ws/handler/jarlog"),拦截地址,但是又没有对应的拦截器。
这里,我在想用自定义WebSocketHandler实现类算了,跟ServerEndpoint功能一毛一样。
二、新处理方式,另外一种websocket消息处理方式
放弃@ServerEndpoint,用自定义实现WebSocketHandler接口处理消息。
自定义TokenWebSocketHandler,在对应的连接、发送消息、异常、关闭连接对应把逻辑添加进去,由于TokenWebSocketHandler是单例(不像@ServerEndpoint,每次访问都是一个新实例),所以要全局变量存储每一个连接对应,关闭后释放。
package com.sakyoka.test.webscoketlog.websocket;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import com.sakyoka.test.utils.Command;
import com.sakyoka.test.webscoketlog.model.WebSocketBean;
import lombok.extern.log4j.Log4j;
/**
*
* 描述:@see WebSocketLog 对应改造。使用WebSocketHandler处理消息
* @author sakyoka
* @date 2022年9月2日 下午4:15:24
*/
@Log4j
@Component
public class TokenWebSocketHandler implements WebSocketHandler{
/**存储session id 对应的数据*/
private static final Map<String, WebSocketBean> sessionLogSocketBeanMap =
new ConcurrentHashMap<String, WebSocketBean>(24);
private static final ExecutorService EXECUTOR_SERVICE = Executors.newCachedThreadPool();
public static final String SYSTEM_FLAG = "system";
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
WebSocketBean jarLogWebSocketBean = new WebSocketBean();
jarLogWebSocketBean.setSessionId(session.getId());
sessionLogSocketBeanMap.put(session.getId(), jarLogWebSocketBean);
Map<String, Object> paramsMap = session.getAttributes();
String jarId = (String)paramsMap.get("jarId");
jarLogWebSocketBean.setJarId(jarId);
log.debug(String.format("接收到jarId:%s, sessionid:%s", jarId, session.getId()));
if (paramsMap.containsKey("showMessage")){
boolean showMessage = Boolean.valueOf(paramsMap.get("showMessage").toString());
jarLogWebSocketBean.setShowMessage(showMessage);
}
String logPath = null;
if (SYSTEM_FLAG.equals(jarId)) {
//logging.file=D:\\system.log
logPath = "D:\\system.log";
}else {
if (paramsMap.containsKey("logPath")){
logPath = (String)paramsMap.get("logPath");
}
//或 根据jarId 获取日志文件路径
}
//window系统 tail命令需要添加tail.exe小工具到system32
String cmd = "tail -f " +logPath;
log.debug(String.format("show log cmd >> %s", cmd));
log.debug(String.format("show log cmd >> %s", cmd));
Command command = Command. getBuilder().commandStr(cmd).autoReadStream(false);
command.exec();
Process process = command.getProcess();
jarLogWebSocketBean.setProcess(process);
InputStream inputStream = process.getInputStream();
jarLogWebSocketBean.setInputStream(inputStream);
EXECUTOR_SERVICE.execute(() -> {
String line;
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(inputStream));
while((line = reader.readLine()) != null) {
session.sendMessage(new TextMessage(line + "<br>"));
}
} catch (IOException e) {
}
});
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
if (sessionLogSocketBeanMap.containsKey(session.getId())) {
WebSocketBean jarLogWebSocketBean = sessionLogSocketBeanMap.get(session.getId());
if (jarLogWebSocketBean.getShowMessage()) {
log.debug(String.format("socket onmessage ==> 接收到信息:%s", message.getPayload()));
}
}
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
if (sessionLogSocketBeanMap.containsKey(session.getId())) {
log.debug(String.format("socket异常,errorMessage:%s", exception.getMessage()));
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
WebSocketBean webSocketBean = sessionLogSocketBeanMap.get(session.getId());
if (Objects.nonNull(webSocketBean.getProcess())) {
webSocketBean.getProcess().destroy();
}
if (Objects.nonNull(webSocketBean.getInputStream())) {
try {
webSocketBean.getInputStream().close();
} catch (Exception e) {}
}
webSocketBean = null;
log.debug(String.format("socket已关闭,sessionid:%s, closeStatus:%s" , session.getId(), closeStatus));
sessionLogSocketBeanMap.remove(session.getId());
}
@Override
public boolean supportsPartialMessages() {
return false;
}
}
自定义WebSocketReadLogInterceptor拦截器,实现HandshakeInterceptor接口,只要处理握手前的方法即可,校验token
package com.sakyoka.test.systemconfig.interceptor;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import com.sakyoka.test.systemconfig.result.ResultBean;
import com.sakyoka.test.utils.JwtTokenUtils;
import lombok.extern.log4j.Log4j;
/**
*
* 描述:websocket 拦截
* @author sakyoka
* @date 2022年9月2日 下午3:51:53
*/
@Component
@Log4j
public class WebSocketReadLogInterceptor implements HandshakeInterceptor{
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws Exception {
HttpServletRequest rs = ((ServletServerHttpRequest) request).getServletRequest();
HttpServletResponse hp = ((ServletServerHttpResponse)response).getServletResponse();
String token = rs.getParameter("token");
if (StringUtils.isBlank(token)){
log.info("token is null");
hp.setStatus(Integer.valueOf(ResultBean.UNAUTHORIZED));
return false;
}
//判断token有效性
boolean tokenValid = JwtTokenUtils.tokenValid(token);
if (!tokenValid){
log.info("invalid token ");
hp.setStatus(Integer.valueOf(ResultBean.UNAUTHORIZED));
return false;
}
//request 参数放入 attributes中
rs.getParameterMap().forEach((k, v) -> {
attributes.put(k, v[0]);
});
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Exception exception) {
}
}
添加处理器、添加拦截地址、添加拦截器
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.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.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
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);
}
}
页面处理
/websocket/log地址改成/ws/websocket/log,追加token参数
<%@ 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 +'/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,
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>
三、测试
1、带token的
socket读取日志信息正常现实。
2、不带token
可以看到,页面日志现实失败,后台打印信息,token is null
拦截ok!
总结
1、是不是发现其实WebSocketHandler就是@ServerEndpoint的另外一种集成websocket方式呢,所以拦截地址才没有生效
registry.addHandler(tokenWebSocketHandler, "/ws/websocket/log")
.setAllowedOrigins("*")
.addInterceptors(webSocketReadLogInterceptor);
tokenWebSocketHandler对应的请求地址就是/ws/websocket/log
附件
springboot+jwt+websocket+拦截-Java文档类资源-CSDN下载
拓展
有兴趣可以看看,最初springboot websocket集成实时读取日志、消息广播、登录认证
springboot集成websocket 实时输出日志到浏览器(一)_sakyoka的博客-CSDN博客_websocket实时显示日志
springboot集成websocket 清空日志后消息广播通知前端重新连接(二)_sakyoka的博客-CSDN博客
springboot 集成JWT 登录验证_sakyoka的博客-CSDN博客
都是在一个项目上慢慢添加的功能。