pwnable.kr第二遍---passcode

>>>>>passcode
1.题目中有两个scanf("%d",addr)的不标准格式
按照scanf()的使用,应该是将stdin的整型数赋值给地址addr处


而接下来的passcode1==338150&&passcode2=13371337两个判断条件
其中passcode1和passcode2是整型


int addr;
假如输入100
则*addr=100
那么addr?


而且如何修改addr值?


2.lea mov
lea(load effective address)
lea eax,[ebx+8]就是将ebx+8这个值直接赋给eax,而不是把ebx+8处的内存地址里的数据赋给eax。
而mov指令则恰恰相反,例如:
mov eax,[ebx+8]则是把内存地址为ebx+8处的数据赋给eax。


在本题中;如果是使用正确的scanf
则应该为:lea -0x70(%ebp),%edx
把ebp-0x70的值放在edx
否则为:mov -0x10(%ebp),%edx
把(ebp-0x10)地址存放的内容放在edx


于是详解scanf():
mov $0x80487dd,%eax //eax里边存放的是用户输入
lea -0x70(%ebp),%edx //edx存放的是一个地址
mov %edx,0x4(%esp) //将一个地址写入给esp+4指向的的位置
mov %eax,(%esp) //将eax中的内容写入esp指向的的位置
首先将$0x80487dd地址处的内容给eax
将-0x70(%ebp)这一地址给edx
然后将edx的内容给esp+4
再将eax的内容给esp


所以scanf("%d",&a)与scanf("%d",a)在第二句中不一样


3.objdump -R passcode
显示文件的动态重定向入口;也就显示了那些动态链接时才加载的函数信息
如printf scanf fflush等
objdump


4.GS保护
检测某些覆盖函数返回地址、异常处理程序地址或特定类型的参数的缓冲区溢出。


5.GOT表&&如何看GOT区域
IDA




6.内存区域可写的部分??????
代码段(.text):存放
数据段(.data)
BSS段


7.mov %eax (%esp)
esp带括号与否?
eax...通用寄存器与Esp ebp之间的区别
ext段在内存中被映射为只读,但.data和.bss是可写的。
堆栈中哪些是可写的?

8.python -c 


9.如何输入??????


10.system()函数的开始是从0x080485e3开始的而不是ea!!


11.fflush和printf都可以;但是scanf()函数的GOT表不可以


思路详解:
main
esp字节对齐?
esp-10;然后将返回地址压栈
调用函数;指令后边是函数入口地址
将。。。给esp??两个之间相差了40


welcome()
push ebp 新的ebp
0x88的距离处,设置栈顶esp
esp            0xffc25a00 0xffc25a00
ebp            0xffc25a88 0xffc25a88
GS保护
eax(函数返回地址入栈) ebp-0xc
清空eax


将要输出的内容地址给esp,借用eax?
然后调用printf函数


将要输出的内容给esp,借用eax...这里不是这个作用了。。。
将要输入的内容给esp+4的位置,借用edx;ebp-0x70
将要输出的内容地址??给esp,借用eax


将之前的ebp-0xc的直至给eax
最后几句不明白??

login()
esp            0xffc25a60 0xffc25a60
ebp            0xffc25a88 0xffc25a88

push ebp 当前esp作为新的ebp
0x28处 设置栈顶Esp


将要输出的内容借用eax给esp,然后调用printf函数


scanf一般都有四条指令,用到esp,ebp,eax,edx
两条用来存储输入的字符串,两条用来定位目的地址
这里将内容输入到ebp-0x10位置处
输入的内容由Eax交给(%esp)

接下来是两条调用??作用 fflush前边的

然后输出字符串,借用eax
接下来又是scanf指令四条
这次将内容存储到ebp-0xc的位置处

接下来是比较指令,如果不想等则跳转
否则调用位于0x8048460的system()函数

由于第一个字符串的scanf()函数中确定只能接收100长度的字符串
因此只能利用第一个初始化passcode1
然后利用第二个scanf函数将passcode1的内容修改即可

这里的思路是
我们在输入passcode1之后,调用system函数之前一拥有fflush printf scanf函数可以利用
这里如果选用fflush函数的话
修改他的GOT表为system函数的入口地址
这样一来当调用fflush函数时,就调用了system()地址

于是目标转换为,改写GOT-fflush地址内容
那么需要找到存放fflush地址的地方?

可以看到当调用fflush()时会跳转到0x804a004存放的地址
jmp    *0x804a004
那么我们可以令:(*0x804a004)=addr(system())

下边的目标就是寻找system()地址0x8048460

于是就可以搞了。。。



12.汇编学习
mov ax,[bx]是指将ds:bx处的数据放入ax 
mov ax,bx是指将bx中的数放入ax

AT&T: -4(%ebp)                         //相当于 Intel: [ebp - 4]
AT&T: foo(,%eax,4)                     //相当于 Intel: [foo + eax*4]
AT&T: foo(,1)                          //相当于 Intel:[foo]
AT&T: %gs:foo                          //相当于 Intel:gs:foo
AT&T: movl -4(%ebp), %eax              //相当于 Intel: mov eax, [ebp - 4]
AT&T: movl array(, %eax, 4), %eax      //相当于 Intel: mov eax, [eax*4 + array]
AT&T: movw array(%ebx, %eax, 4), %cx   //相当于 Intel: mov cx, [ebx + 4*eax + array]
AT&T: movb $4, %fs:(%eax)              //相当于 Intel: mov fs:eax, 4
package com.jsfj.business.websocket.config; import com.jsfj.business.websocket.config.Factory.CustomWebSocketHandlerDecoratorFactory; import com.jsfj.business.websocket.data.ActiveWebSocketUserRepository; import com.jsfj.business.websocket.handler.WebSocketConnectHandler; import com.jsfj.business.websocket.handler.WebSocketDisconnectHandler; import com.jsfj.business.websocket.handler.WsUser; import com.jsfj.commons.core.security.User; import com.jsfj.commons.security.CertifiedHeaderHelper; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.converter.MappingJackson2MessageConverter; import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.config.ChannelRegistration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.messaging.simp.stomp.StompCommand; import org.springframework.messaging.simp.stomp.StompHeaderAccessor; import org.springframework.messaging.support.ChannelInterceptor; import org.springframework.messaging.support.MessageHeaderAccessor; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration; import org.springframework.web.socket.messaging.SessionConnectEvent; import org.springframework.web.socket.messaging.SessionDisconnectEvent; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean; import javax.annotation.Resource; import java.util.List; import java.util.Map; import java.util.Objects; @Slf4j @Configuration @EnableWebSocketMessageBroker @Order(Ordered.HIGHEST_PRECEDENCE + 99) // 确保配置高优先级 public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { // 外部化配置参数 @Value("${stomp.broker.relay.host:mq.master.jshi9.com}") private String relayHost; @Value("${stomp.broker.relay.port:61613}") private int relayPort; @Value("${stomp.broker.client.login:jsfj_stomp}") private String clientLogin; @Value("${stomp.broker.client.passcode:jsfj_stomp}") private String clientPasscode; @Value("${stomp.broker.system.login:jsfj_stomp}") private String systemLogin; @Value("${stomp.broker.system.passcode:jsfj_stomp}") private String systemPasscode; @Value("${stomp.broker.virtualHost:/dev}") private String virtualHost; @Resource private TokenStore tokenStore; // 1. 增强端点注册安全 @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/connector/**") .setAllowedOriginPatterns("*") // 禁用不安全传输方式 //.setHandshakeHandler(new CustomHandshakeHandler()) .withSockJS() .setSupressCors(true); // 明确禁止CORS registry.addEndpoint("/sock-js") .setAllowedOriginPatterns("*") .withSockJS(); registry.addEndpoint("/web-socket") .setAllowedOriginPatterns("*"); //.addInterceptors(new IpHandshakeInterceptor()); // IP过滤 } // 2. 优化认证拦截器 @Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors(new AuthChannelInterceptor()); } /** * 专用认证拦截器 (线程安全) */ private class AuthChannelInterceptor implements ChannelInterceptor { @Override public Message<?> preSend(Message<?> message, MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) { // 优先使用token认证 String token = getHeaderInMessage(message, "Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); User user = CertifiedHeaderHelper.getUser(token); if (user != null) { accessor.setUser(new WsUser(String.valueOf(user.getUserId()), user.getUserType())); return message; } } token = getHeaderInMessage(message, "token"); if (token != null && !token.startsWith("Bearer ")) { User user = CertifiedHeaderHelper.getUser(token); if (user != null) { accessor.setUser(new WsUser(String.valueOf(user.getUserId()), user.getUserType())); return message; } } // 备用方案:userID直接认证(生产环境应禁用) List<String> userIds = accessor.getNativeHeader("userId"); if (userIds != null && !userIds.isEmpty()) { String userId = userIds.get(0); accessor.setUser(new WsUser(userId, Objects.requireNonNull(accessor.getNativeHeader("type")).get(0))); } else { log.warn("WebSocket连接缺少认证信息"); return null; // 直接拒绝连接 } } return message; } } // 3. 消息代理优化配置 @Override public void configureMessageBroker(MessageBrokerRegistry registry) { // 开发/单机环境可以使用简单代理 // registry.enableSimpleBroker("/topic/", "/queue/", "/user/"); // registry.setApplicationDestinationPrefixes("/app"); //外部代理 //【首要任务】:在应用服务器上执行 telnet mq.master.jshi9.com 61613,排除网络问题。 //【确认配置】:在 RabbitMQ 服务器上执行 rabbitmq-plugins list | grep rabbitmq_stomp,确认插件已启 // # 为用户 jsfj_app 授予对虚拟主机 jsfj_stomp 的配置、写、读权限 # ".*" 表示对所有队列和交换机都有权限 // rabbitmqctl set_permissions -p dev jsfj_stomp ".*" ".*" ".*" // # 执行启用与重启 rabbitmq-plugins enable rabbitmq_stomp // sudo systemctl restart rabbitmq-server registry.enableStompBrokerRelay("/topic/", "/queue/", "/user/") .setRelayHost(relayHost) .setRelayPort(relayPort) .setClientLogin(clientLogin) .setClientPasscode(clientPasscode) .setSystemLogin(systemLogin) .setSystemPasscode(systemPasscode) .setVirtualHost(virtualHost) .setTaskScheduler(heartBeatScheduler()) // 心跳设置 (单位:毫秒) .setSystemHeartbeatSendInterval(5000) .setSystemHeartbeatReceiveInterval(4000); registry.setApplicationDestinationPrefixes("/app"); //registry.setUserDestinationPrefix("/user"); } // 4. 传输层优化 @Override public void configureWebSocketTransport(WebSocketTransportRegistration registration) { registration .setMessageSizeLimit(128 * 1024) // 128KB .setSendBufferSizeLimit(1024 * 1024) // 1MB .setSendTimeLimit(30 * 1000) // 30秒 //添加我们的自定义装饰器工厂 .setDecoratorFactories(new CustomWebSocketHandlerDecoratorFactory( )); } // 5. 容器级配置(底层TCP参数) @Bean public ServletServerContainerFactoryBean createWebSocketContainer() { ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); container.setMaxTextMessageBufferSize(128 * 1024); container.setMaxBinaryMessageBufferSize(128 * 1024); container.setMaxSessionIdleTimeout(30 * 60 * 1000L); // 30分钟 return container; } // 6. 消息转换器优化(JSON序列化) @Override public boolean configureMessageConverters(List<MessageConverter> messageConverters) { MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(); converter.setStrictContentTypeMatch(true); messageConverters.add(converter); return true; // 不使用默认转换器 } // 7. 线程池精细化配置 @Bean public ThreadPoolTaskScheduler heartBeatScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(2); // 增加线程容错 scheduler.setThreadNamePrefix("ws-hb-"); scheduler.setDaemon(true); scheduler.setAwaitTerminationSeconds(30); return scheduler; } // 工具方法优化 private String getHeaderInMessage(Message<?> message, String headerName) { if (message == null) { return null; } Object nativeHeaders = message.getHeaders().get(SimpMessageHeaderAccessor.NATIVE_HEADERS); if (!(nativeHeaders instanceof Map)) return null; Object values = ((Map<?, ?>) nativeHeaders).get(headerName); if (values instanceof List && !((List<?>) values).isEmpty()) { return ((List<?>) values).get(0).toString(); } return null; } @Bean public WebSocketConnectHandler<SessionConnectEvent> webSocketConnectHandler(ActiveWebSocketUserRepository repository) { return new WebSocketConnectHandler<>(repository); } @Bean public WebSocketDisconnectHandler<SessionDisconnectEvent> webSocketDisconnectHandler(ActiveWebSocketUserRepository repository) { return new WebSocketDisconnectHandler<>(repository); } } 2025-11-26 17:55:54.275 [http-nio-21006-exec-4] [DEBUG] [org.springframework.web.servlet.handler.AbstractHandlerMapping:522] - Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@6b44121e 2025-11-26 17:55:54.276 [http-nio-21006-exec-4] [DEBUG] [org.springframework.web.socket.sockjs.support.AbstractSockJsService:412] - Processing transport request: GET http://192.168.0.39:21006/sock-js/info?t=1764150932682 2025-11-26 17:55:54.276 [http-nio-21006-exec-9] [DEBUG] [org.springframework.web.servlet.handler.AbstractHandlerMapping:522] - Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@6b44121e 2025-11-26 17:55:54.276 [http-nio-21006-exec-8] [DEBUG] [org.springframework.web.servlet.handler.AbstractHandlerMapping:522] - Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@6b44121e 2025-11-26 17:55:54.277 [http-nio-21006-exec-4] [DEBUG] [org.springframework.web.servlet.FrameworkServlet:1131] - Completed 200 OK 2025-11-26 17:55:54.278 [http-nio-21006-exec-8] [DEBUG] [org.springframework.core.log.LogFormatUtils:119] - GET "/sock-js/info?t=1764150952689", parameters={t:[1764150952689]} 2025-11-26 17:55:54.278 [http-nio-21006-exec-9] [DEBUG] [org.springframework.core.log.LogFormatUtils:119] - GET "/sock-js/info?t=1764150942694", parameters={t:[1764150942694]} 2025-11-26 17:55:54.281 [http-nio-21006-exec-8] [DEBUG] [org.springframework.web.servlet.handler.AbstractHandlerMapping:522] - Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@6b44121e 2025-11-26 17:55:54.281 [http-nio-21006-exec-9] [DEBUG] [org.springframework.web.servlet.handler.AbstractHandlerMapping:522] - Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@6b44121e 2025-11-26 17:55:54.282 [http-nio-21006-exec-8] [DEBUG] [org.springframework.web.socket.sockjs.support.AbstractSockJsService:412] - Processing transport request: GET http://192.168.0.39:21006/sock-js/info?t=1764150952689 2025-11-26 17:55:54.282 [http-nio-21006-exec-9] [DEBUG] [org.springframework.web.socket.sockjs.support.AbstractSockJsService:412] - Processing transport request: GET http://192.168.0.39:21006/sock-js/info?t=1764150942694 2025-11-26 17:55:54.283 [http-nio-21006-exec-9] [DEBUG] [org.springframework.web.servlet.FrameworkServlet:1131] - Completed 200 OK 2025-11-26 17:55:54.283 [http-nio-21006-exec-8] [DEBUG] [org.springframework.web.servlet.FrameworkServlet:1131] - Completed 200 OK 2025-11-26 17:55:54.395 [http-nio-21006-exec-6] [DEBUG] [org.springframework.web.servlet.handler.AbstractHandlerMapping:522] - Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@6b44121e 2025-11-26 17:55:54.396 [http-nio-21006-exec-6] [DEBUG] [org.springframework.core.log.LogFormatUtils:119] - GET "/sock-js/310/ogh3yq34/websocket", parameters={} 2025-11-26 17:55:54.398 [http-nio-21006-exec-6] [DEBUG] [org.springframework.web.servlet.handler.AbstractHandlerMapping:522] - Mapped to org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler@6b44121e 2025-11-26 17:55:54.399 [http-nio-21006-exec-6] [DEBUG] [org.springframework.web.socket.sockjs.support.AbstractSockJsService:482] - Processing transport request: GET http://192.168.0.39:21006/sock-js/310/ogh3yq34/websocket 2025-11-26 17:55:54.400 [http-nio-21006-exec-6] [DEBUG] [org.springframework.web.servlet.FrameworkServlet:1131] - Completed 101 SWITCHING_PROTOCOLS 2025-11-26 17:55:54.401 [http-nio-21006-exec-6] [DEBUG] [org.springframework.web.socket.handler.LoggingWebSocketHandlerDecorator:46] - New WebSocketServerSockJsSession[id=ogh3yq34] 2025-11-26 17:55:54.401 [http-nio-21006-exec-6] [INFO] [com.jsfj.business.websocket.config.Factory.CustomWebSocketHandlerDecorator:42] - WebSocket connection established. SessionId: ogh3yq34, RemoteAddress: /192.168.5.66:43530, User: null, TotalActiveConnections: 1, TotalConnections: 9 2025-11-26 17:55:54.412 [clientInboundChannel-35] [DEBUG] [org.springframework.core.log.CompositeLog:127] - CONNECT user=1956172433130041346 session=ogh3yq34 2025-11-26 17:55:54.412 [http-nio-21006-exec-1] [INFO] [com.jsfj.business.websocket.handler.WebSocketConnectHandler:43] - onLine ogh3yq34 2025-11-26 17:55:57.683 [clientInboundChannel-38] [DEBUG] [org.springframework.core.log.CompositeLog:127] - TCP connection closed already, ignoring DISCONNECT session=ogh3yq34 2025-11-26 17:55:57.685 [http-nio-21006-exec-5] [DEBUG] [org.springframework.web.socket.handler.LoggingWebSocketHandlerDecorator:70] - WebSocketServerSockJsSession[id=ogh3yq34] closed with CloseStatus[code=1000, reason=null] 2025-11-26 17:55:57.685 [http-nio-21006-exec-5] [ERROR] [com.jsfj.business.websocket.config.Factory.CustomWebSocketHandlerDecorator:58] - WebSocket connection closed. SessionId: ogh3yq34, CloseStatus: CloseStatus[code=1000, reason=null], TotalActiveConnections: 0 2025-11-26 17:55:57.687 [http-nio-21006-exec-5] [DEBUG] [org.springframework.web.socket.messaging.SubProtocolWebSocketHandler:525] - Clearing session ogh3yq34 2025-11-26 17:55:57.687 [http-nio-21006-exec-5] [INFO] [com.jsfj.business.websocket.handler.WebSocketDisconnectHandler:38] - outLine ogh3yq34 2025-11-26 17:55:57.689 [clientInboundChannel-41] [DEBUG] [org.springframework.core.log.CompositeLog:127] - TCP connection closed already, ignoring DISCONNECT session=ogh3yq34
最新发布
11-27
<think> 我们注意到在日志中,用户连接时认证失败(User: null),然后在连接建立后,在`clientInboundChannel-35`中处理CONNECT消息时,用户ID被正确设置(user=1956172433130041346)。但是,在连接关闭时,日志显示“TCP connection closed already, ignoring DISCONNECT session=ogh3yq34”,并且连接被正常关闭(CloseStatus[code=1000])。 然而,在连接建立时的日志中,我们看到了一个潜在的问题: 2025-11-26 17:55:54.401 [http-nio-21006-exec-6] [INFO] [com.jsfj.business.websocket.config.Factory.CustomWebSocketHandlerDecorator:42] - WebSocket connection established. SessionId: ogh3yq34, RemoteAddress: /192.168.5.66:43530, User: null, TotalActiveConnections: 1, TotalConnections: 9 这里User为null,说明在WebSocket连接建立时(即握手阶段)用户信息尚未被设置。但是,在后续的CONNECT消息处理中(即STOMP协议层的连接)用户信息被设置(user=1956172433130041346)。这符合STOMP的工作流程:WebSocket连接建立后,客户端会发送一个STOMP CONNECT帧,此时我们通过`AuthChannelInterceptor`进行认证并设置用户。 因此,连接建立时的User为null是正常的,因为此时STOMP连接还未建立。 但是,我们注意到在连接关闭时,我们记录了两条关于DISCONNECT的消息,且都提示“TCP connection closed already”。这可能是因为连接已经被客户端关闭,而服务器在清理资源时试图再次关闭。 另外,在连接关闭后,我们看到了`WebSocketDisconnectHandler`被触发,记录了下线信息。 所以,从日志上看,连接建立和断开的过程基本正常。 但是,有一个问题需要关注:在连接建立后,STOMP CONNECT帧处理时,我们设置了用户,但是在连接关闭时,我们并没有在日志中看到用户信息(只有session id)。如果我们希望在连接关闭时记录用户信息,我们需要在`WebSocketDisconnectHandler`中从session中获取用户。 我们来看一下`WebSocketDisconnectHandler`的实现: 在`WebSocketDisconnectHandler`中,我们处理`SessionDisconnectEvent`事件。这个事件包含`Message<DisconnectEvent>`,我们可以从中获取用户: ```java public class WebSocketDisconnectHandler<T> implements ApplicationListener<T> { // ... @Override public void onApplicationEvent(T event) { if (event instanceof SessionDisconnectEvent) { SessionDisconnectEvent disconnectEvent = (SessionDisconnectEvent) event; String sessionId = disconnectEvent.getSessionId(); // 获取用户 Principal user = disconnectEvent.getUser(); // 如果user不为null,可以获取用户名 if (user != null) { String username = user.getName(); // 记录日志 log.info("用户 {} 断开连接,SessionId: {}", username, sessionId); } else { log.info("SessionId: {} 断开连接,但用户未认证", sessionId); } // ... 从repository中移除 } } } ``` 因此,我们需要检查`SessionDisconnectEvent`中是否能够获取到用户。如果获取不到,可能是因为在连接关闭时,用户信息已经丢失。 另外,在连接关闭时,我们看到了两条DISCONNECT日志,这可能是由于STOMP和WebSocket协议层都触发了断开事件。我们需要确保我们的断开处理是幂等的。 建议: 1. 在`WebSocketDisconnectHandler`中,记录断开事件时同时记录用户信息(如果有的话),以便排查问题。 2. 确保`ActiveWebSocketUserRepository`中移除连接的操作是幂等的,避免重复移除导致的问题。 此外,我们还注意到在连接建立时,我们记录的总连接数(TotalConnections)为9,而活跃连接数(TotalActiveConnections)为1。这可能是由于之前的连接没有正确清理,或者是统计方式的问题。需要检查`CustomWebSocketHandlerDecoratorFactory`和`ActiveWebSocketUserRepository`的实现,确保连接计数准确。 如果连接计数不准确,可能会导致内存泄漏或错误的统计信息。 另外,关于连接关闭时出现两次DISCONNECT日志,可能是由于STOMP协议和WebSocket协议层都触发了断开事件。我们可以在`WebSocketDisconnectHandler`中检查事件类型,确保只处理一次(例如,只处理`SessionDisconnectEvent`,而忽略其他)。 总结一下,从日志来看,连接建立和断开是正常的,但我们需要: 1. 在断开事件中记录用户信息。 2. 确保连接计数的准确性。 3. 确保断开处理是幂等的。 如果问题仍然存在,请提供更多关于断开连接时的问题描述(例如,客户端是否正常关闭?是否有异常?)。 根据以上分析,我们不需要修改配置类,但需要调整日志记录和断开处理逻辑。 现在,让我们调整`WebSocketDisconnectHandler`,在日志中记录用户信息: 修改`WebSocketDisconnectHandler`的`onApplicationEvent`方法: ```java @Slf4j public class WebSocketDisconnectHandler<T> implements ApplicationListener<T> { private final ActiveWebSocketUserRepository repository; public WebSocketDisconnectHandler(ActiveWebSocketUserRepository repository) { this.repository = repository; } @Override public void onApplicationEvent(T event) { if (event instanceof SessionDisconnectEvent) { SessionDisconnectEvent disconnectEvent = (SessionDisconnectEvent) event; String sessionId = disconnectEvent.getSessionId(); Principal principal = disconnectEvent.getUser(); String userId = principal != null ? principal.getName() : "unknown"; log.info("用户 {} 断开连接,SessionId: {}", userId, sessionId); // 从repository中移除 repository.remove(sessionId); } } } ``` 同时,确保`ActiveWebSocketUserRepository`中的`remove`方法是线程安全的,并且可以多次调用(幂等)。 另外,在`CustomWebSocketHandlerDecorator`中,我们记录连接建立和关闭。在连接建立时,由于此时STOMP连接还未建立,所以用户信息为null是正常的,我们可以记录session id和远程地址。在连接关闭时,我们记录session id和关闭状态。 如果连接关闭时,我们想记录用户信息,我们可以在`CustomWebSocketHandlerDecorator`中覆盖`afterConnectionClosed`方法,从session属性中获取用户(因为STOMP连接建立后,会设置用户属性): ```java public class CustomWebSocketHandlerDecorator extends WebSocketHandlerDecorator { // ... @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception { // 尝试从session中获取用户 Principal user = session.getPrincipal(); String userId = user != null ? user.getName() : "unknown"; log.info("WebSocket connection closed. SessionId: {}, User: {}, CloseStatus: {}", session.getId(), userId, closeStatus); super.afterConnectionClosed(session, closeStatus); } } ``` 这样,在连接关闭时,我们就可以在`CustomWebSocketHandlerDecorator`中记录用户信息。 综上所述,我们调整两个地方的日志记录: 1. 在`WebSocketDisconnectHandler`中,记录断开事件的用户信息。 2. 在`CustomWebSocketHandlerDecorator`的`afterConnectionClosed`方法中,记录用户信息。 这样,在连接关闭时,我们可以在两个地方看到用户信息,便于追踪。 如果问题仍然存在,请检查STOMP代理中继(RabbitMQ)的配置和网络连接,确保STOMP连接能够正常建立和关闭。 另外,关于两次DISCONNECT日志,可能是正常的协议处理流程,只要不影响业务,可以忽略。如果确实需要避免重复日志,可以在`AuthChannelInterceptor`中忽略DISCONNECT帧(因为连接已经关闭,无法处理)。 由于目前没有明确的错误,我们主要进行日志增强以便后续排查。 如果用户反馈连接断开,我们可以根据增强的日志查看用户断开连接的原因和用户信息。 我们不需要修改配置类`WebSocketConfig`,但需要修改`WebSocketDisconnectHandler`和`CustomWebSocketHandlerDecorator`的日志记录。 请根据上述建议调整代码,并观察日志。 如果问题仍然存在,请提供更详细的错误信息或重现步骤。 </think>您的 WebSocket 配置基本完整,但根据日志分析有几个关键问题需要优化。以下是完整的解决方案: ### 问题诊断与修复方案 #### 1. 认证信息未正确传递 (关键问题) 日志显示 `User: null`,说明认证信息未正确注入: ```log 2025-11-26 17:55:54.401 [INFO] WebSocket connection established. SessionId: ogh3yq34, User: null ``` **修复方案**: ```java // 在 configureClientInboundChannel 中增强认证逻辑 @Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors(new ChannelInterceptor() { @Override public Message<?> preSend(Message<?> message, MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) { // 1. 优先从WebSocketSession属性获取认证信息 SimpMessageHeaderAccessor simpAccessor = SimpMessageHeaderAccessor.wrap(message); if (simpAccessor.getUser() != null) { return message; } // 2. 增强的token解析逻辑 String token = resolveToken(message); if (token != null) { try { User user = CertifiedHeaderHelper.getUser(token); accessor.setUser(new WsUser(user.getUserId(), user.getUserType())); } catch (AuthenticationException e) { log.warn("Token验证失败: {}", e.getMessage()); return null; // 拒绝连接 } } } return message; } private String resolveToken(Message<?> message) { // 多位置尝试获取token String token = getHeaderInMessage(message, "Authorization"); if (token == null) token = getHeaderInMessage(message, "token"); if (token == null) token = getNativeHeader(message, "Sec-WebSocket-Protocol"); return token != null ? token.replace("Bearer ", "") : null; } }); } ``` #### 2. STOMP 心跳配置问题 日志显示 TCP 连接已关闭但仍在尝试发送 DISCONNECT: ```log 2025-11-26 17:55:57.683 [DEBUG] TCP connection closed already, ignoring DISCONNECT ``` **优化心跳配置**: ```java @Override public void configureMessageBroker(MessageBrokerRegistry registry) { ThreadPoolTaskScheduler scheduler = heartBeatScheduler(); registry.enableStompBrokerRelay("/topic", "/queue", "/user") .setRelayHost(relayHost) .setRelayPort(relayPort) // 增加心跳容错 .setSystemHeartbeatSendInterval(5000) .setSystemHeartbeatReceiveInterval(7000) // 大于发送间隔 .setTaskScheduler(scheduler) .setAutoStartup(true); } @Bean public ThreadPoolTaskScheduler heartBeatScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(4); // 增加线程数 scheduler.setErrorHandler(t -> log.error("Heartbeat error", t)); return scheduler; } ``` #### 3. WebSocketSession 生命周期管理 连接关闭时未正确清理资源: ```log 2025-11-26 17:55:57.685 [ERROR] WebSocket connection closed. SessionId: ogh3yq34 ``` **增强 Session 管理**: ```java // 在 CustomWebSocketHandlerDecorator 中添加 public class CustomWebSocketHandlerDecorator extends WebSocketHandlerDecorator { private final ActiveWebSocketUserRepository repository; @Override public void afterConnectionEstablished(WebSocketSession session) { // 保存session到ThreadLocal WebSocketSessionHolder.setSession(session); super.afterConnectionEstablished(session); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { try { String sessionId = session.getId(); // 确保清理资源 repository.remove(sessionId); log.info("Cleaned resources for session: {}", sessionId); } finally { WebSocketSessionHolder.clear(); super.afterConnectionClosed(session, status); } } } // Session 持有器 public class WebSocketSessionHolder { private static final ThreadLocal<WebSocketSession> holder = new ThreadLocal<>(); public static void setSession(WebSocketSession session) { holder.set(session); } public static WebSocketSession getSession() { return holder.get(); } public static void clear() { holder.remove(); } } ``` ### 完整优化配置 ```java @Configuration @EnableWebSocketMessageBroker @Order(Ordered.HIGHEST_PRECEDENCE + 99) public class OptimizedWebSocketConfig implements WebSocketMessageBrokerConfigurer { // ... [其他配置保持不变] ... @Override public void configureClientInboundChannel(ChannelRegistration registration) { registration.interceptors(new EnhancedAuthInterceptor()); } private class EnhancedAuthInterceptor implements ChannelInterceptor { private final AntPathMatcher pathMatcher = new AntPathMatcher(); @Override public Message<?> preSend(Message<?> message, MessageChannel channel) { StompHeaderAccessor accessor = getStompAccessor(message); if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) { // 特殊路径跳过认证 String destination = accessor.getDestination(); if (pathMatcher.match("/topic/public/**", destination)) { return message; } WsUser user = resolveUser(message); if (user == null) { log.warn("Authentication failed for session: {}", accessor.getSessionId()); throw new AuthenticationCredentialsNotFoundException("Missing credentials"); } accessor.setUser(user); } return message; } private WsUser resolveUser(Message<?> message) { // [使用前面增强的认证逻辑] } } @Override public void configureWebSocketTransport(WebSocketTransportRegistration registration) { registration .setDecoratorFactories(new CustomWebSocketHandlerDecoratorFactory()) // 增加缓冲区大小 .setMessageSizeLimit(256 * 1024) .setSendBufferSizeLimit(2 * 1024 * 1024); } // 增强的Endpoint配置 @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/connector/**") .setAllowedOriginPatterns("*") .setHandshakeHandler(new TokenHandshakeHandler(tokenStore)) .withSockJS() .setSessionCookieNeeded(true); // 启用Cookie registry.addEndpoint("/sock-js") .setAllowedOriginPatterns("*") .addInterceptors(new IpHandshakeInterceptor()); } // 增强的心跳配置 @Bean public ThreadPoolTaskScheduler heartBeatScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(4); scheduler.setThreadNamePrefix("ws-hb-"); scheduler.setWaitForTasksToCompleteOnShutdown(true); scheduler.setAwaitTerminationSeconds(30); return scheduler; } } ``` ### 关键组件实现 #### 1. TokenHandshakeHandler (握手阶段认证) ```java public class TokenHandshakeHandler extends DefaultHandshakeHandler { private final TokenStore tokenStore; public TokenHandshakeHandler(TokenStore tokenStore) { this.tokenStore = tokenStore; } @Override protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) { // 从请求参数获取token String token = request.getURI().getQuery().split("token=")[1]; // 验证token OAuth2AccessToken accessToken = tokenStore.readAccessToken(token); if (accessToken == null || accessToken.isExpired()) { throw new AuthenticationCredentialsNotFoundException("Invalid token"); } // 提取用户信息 OAuth2Authentication auth = tokenStore.readAuthentication(token); User user = (User) auth.getPrincipal(); return new WsUser(user.getUserId(), user.getUserType()); } } ``` #### 2. WebSocket 连接监控端点 ```java @RestController @RequestMapping("/websocket-monitor") public class WebSocketMonitorController { @Autowired private ActiveWebSocketUserRepository repository; @GetMapping("/connections") public Map<String, Object> getActiveConnections() { return Map.of( "activeCount", repository.getActiveCount(), "totalSessions", repository.getAllSessions() ); } @PostMapping("/close/{sessionId}") public ResponseEntity<?> closeSession(@PathVariable String sessionId) { if (repository.closeSession(sessionId)) { return ResponseEntity.ok().build(); } return ResponseEntity.notFound().build(); } } ``` ### 配置建议 1. **增加网络超时配置**: ```yaml spring: cloud: gateway: httpclient: connect-timeout: 1000 response-timeout: 60s ``` 2. **启用 WebSocket 压缩**: ```java @Override public void configureWebSocketTransport(WebSocketTransportRegistration registration) { registration.setCompressionEnabled(true); } ``` 3. **STOMP 代理中继监控**: ```bash # 监控 RabbitMQ STOMP 连接 rabbitmqctl list_connections name user state timeout ``` 这些优化将解决认证失败、心跳断开和资源清理问题,同时提供更好的监控和管理能力。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值