refactor: websocket 解析令牌支持 Redis+Token 会话

This commit is contained in:
Ray.Hao
2025-04-11 17:16:55 +08:00
parent 3ba704171d
commit cb6b2a4c13

View File

@@ -1,9 +1,8 @@
package com.youlai.boot.config; package com.youlai.boot.config;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.jwt.JWTPayload; import com.youlai.boot.core.security.model.SysUserDetails;
import cn.hutool.jwt.JWTUtil; import com.youlai.boot.core.security.token.TokenManager;
import com.youlai.boot.common.constant.SecurityConstants;
import com.youlai.boot.system.event.UserConnectionEvent; import com.youlai.boot.system.event.UserConnectionEvent;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -12,12 +11,17 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.messaging.Message; import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.simp.config.ChannelRegistration; import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.stomp.StompCommand; import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor; import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor; import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor; 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.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@@ -36,9 +40,13 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final ApplicationEventPublisher eventPublisher; private final ApplicationEventPublisher eventPublisher;
public WebSocketConfig(ApplicationEventPublisher eventPublisher) { private final TokenManager tokenManager;
public WebSocketConfig(ApplicationEventPublisher eventPublisher, TokenManager tokenManager) {
this.eventPublisher = eventPublisher; this.eventPublisher = eventPublisher;
this.tokenManager = tokenManager;
} }
/** /**
* 注册一个端点,客户端通过这个端点进行连接 * 注册一个端点,客户端通过这个端点进行连接
*/ */
@@ -71,9 +79,10 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/** /**
* 配置客户端入站通道拦截器 * 配置客户端入站通道拦截器
* <p> * <p>
* 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户 * 核心功能:
* * 1. 连接建立时解析令牌并绑定用户身份
* @param registration 通道注册器 * 2. 连接关闭时触发下线通知
* 3. 异常Token的防御性处理
*/ */
@Override @Override
public void configureClientInboundChannel(ChannelRegistration registration) { public void configureClientInboundChannel(ChannelRegistration registration) {
@@ -81,24 +90,78 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override @Override
public Message<?> preSend(@NotNull Message<?> message, @NotNull MessageChannel channel) { public Message<?> preSend(@NotNull Message<?> message, @NotNull MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); 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())) { 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()); * 1. 从HEADER中获取Authorization值
String username = JWTUtil.parseToken(bearerToken).getPayloads().getStr(JWTPayload.SUBJECT); * 2. 校验Bearer Token格式合法性
if (StrUtil.isNotBlank(username)) { * 3. 解析并验证JWT有效性
accessor.setUser(() -> username); * 4. 绑定用户身份到当前会话
*/
String authorization = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION);
// 防御性校验确保Authorization头存在且格式正确
if (StrUtil.isBlank(authorization) || !authorization.startsWith("Bearer ")) {
log.warn("非法连接请求缺少有效的Authorization头");
throw new AuthenticationCredentialsNotFoundException("Missing authorization header");
}
// 提取并处理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)); eventPublisher.publishEvent(new UserConnectionEvent(this, username, true));
} }
} // 处理客户端断开请求
} else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) { else if (StompCommand.DISCONNECT.equals(accessor.getCommand())) {
if (accessor.getUser() != null) { /*
String username = accessor.getUser().getName(); * 注意:只有成功建立过认证的连接才会触发下线事件
* 防止未认证成功的连接产生脏数据
*/
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)); 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); return ChannelInterceptor.super.preSend(message, channel);
} }
}); });