diff --git a/src/main/java/com/youlai/boot/config/WebSocketConfig.java b/src/main/java/com/youlai/boot/config/WebSocketConfig.java index c5996780..2d9bbf48 100644 --- a/src/main/java/com/youlai/boot/config/WebSocketConfig.java +++ b/src/main/java/com/youlai/boot/config/WebSocketConfig.java @@ -1,9 +1,8 @@ package com.youlai.boot.config; import cn.hutool.core.util.StrUtil; -import cn.hutool.jwt.JWTPayload; -import cn.hutool.jwt.JWTUtil; -import com.youlai.boot.common.constant.SecurityConstants; +import com.youlai.boot.core.security.model.SysUserDetails; +import com.youlai.boot.core.security.token.TokenManager; import com.youlai.boot.system.event.UserConnectionEvent; import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; @@ -12,12 +11,17 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.MessagingException; 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.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; @@ -36,9 +40,13 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { private final ApplicationEventPublisher eventPublisher; - public WebSocketConfig(ApplicationEventPublisher eventPublisher) { + private final TokenManager tokenManager; + + public WebSocketConfig(ApplicationEventPublisher eventPublisher, TokenManager tokenManager) { this.eventPublisher = eventPublisher; + this.tokenManager = tokenManager; } + /** * 注册一个端点,客户端通过这个端点进行连接 */ @@ -71,9 +79,10 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { /** * 配置客户端入站通道拦截器 *
- * 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 - * - * @param registration 通道注册器 + * 核心功能: + * 1. 连接建立时解析令牌并绑定用户身份 + * 2. 连接关闭时触发下线通知 + * 3. 异常Token的防御性处理 */ @Override public void configureClientInboundChannel(ChannelRegistration registration) { @@ -81,24 +90,78 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public Message> preSend(@NotNull Message> message, @NotNull MessageChannel channel) { StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); - if (accessor != null) { + if (accessor == null) { + return ChannelInterceptor.super.preSend(message, channel); + } + + try { + // 处理客户端连接请求 if (StompCommand.CONNECT.equals(accessor.getCommand())) { - String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); - if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) { - bearerToken = bearerToken.substring(SecurityConstants.BEARER_TOKEN_PREFIX .length()); - String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); - if (StrUtil.isNotBlank(username)) { - accessor.setUser(() -> username); - eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); - } + /* + * 安全校验流程: + * 1. 从HEADER中获取Authorization值 + * 2. 校验Bearer Token格式合法性 + * 3. 解析并验证JWT有效性 + * 4. 绑定用户身份到当前会话 + */ + String authorization = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION); + + // 防御性校验:确保Authorization头存在且格式正确 + if (StrUtil.isBlank(authorization) || !authorization.startsWith("Bearer ")) { + log.warn("非法连接请求:缺少有效的Authorization头"); + throw new AuthenticationCredentialsNotFoundException("Missing authorization header"); } - } else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { - if (accessor.getUser() != null) { - String username = accessor.getUser().getName(); + + // 提取并处理JWT令牌(移除Bearer前缀) + String token = authorization.substring(7); + Authentication authentication = tokenManager.parseToken(token); + + // 令牌解析失败处理 + if (authentication == null) { + log.error("令牌解析失败:{}", token); + throw new BadCredentialsException("Invalid token"); + } + + // 获取用户详细信息 + SysUserDetails userDetails = (SysUserDetails) authentication.getPrincipal(); + if (userDetails == null || StrUtil.isBlank(userDetails.getUsername())) { + log.error("无效的用户凭证:{}", token); + throw new BadCredentialsException("Invalid user credentials"); + } + + String username = userDetails.getUsername(); + log.info("WebSocket连接建立:用户[{}]", username); + + // 绑定用户身份到当前会话(重要:用于@SendToUser等注解) + accessor.setUser(authentication); + + // 发布用户上线事件(示例:可用于更新在线用户列表) + eventPublisher.publishEvent(new UserConnectionEvent(this, username, true)); + + } + // 处理客户端断开请求 + else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { + /* + * 注意:只有成功建立过认证的连接才会触发下线事件 + * 防止未认证成功的连接产生脏数据 + */ + Authentication authentication = (Authentication) accessor.getUser(); + if (authentication != null && authentication.isAuthenticated()) { + String username = ((SysUserDetails) authentication.getPrincipal()).getUsername(); + log.info("WebSocket连接关闭:用户[{}]", username); eventPublisher.publishEvent(new UserConnectionEvent(this, username, false)); } } + } catch (AuthenticationException ex) { + // 认证失败时强制关闭连接 + log.error("连接认证失败:{}", ex.getMessage()); + throw ex; + } catch (Exception ex) { + // 捕获其他未知异常 + log.error("WebSocket连接处理异常:", ex); + throw new MessagingException("Connection processing failed"); } + return ChannelInterceptor.super.preSend(message, channel); } });