修复WebSocket身份认证问题

- 添加WebSocketAuthInterceptor处理token认证
- 修改WebSocket连接逻辑,支持token传递
- 统一用户身份识别,确保登录用户使用USER类型
- 修复前端环境变量配置,统一WebSocket URL
- 添加Token测试页面用于验证功能
- 更新聊天消息处理逻辑,正确识别用户身份

解决了登录用户发送消息时同时保存GUEST和USER两种类型数据的问题
This commit is contained in:
2025-07-24 17:51:38 +08:00
parent 6560e66959
commit 847f5126cf
30 changed files with 1447 additions and 216 deletions
@@ -24,9 +24,9 @@ public class EmotionSimpleApplication {
System.out.println("🎉 情感博物馆服务启动成功!");
System.out.println("📋 服务信息:");
System.out.println(" - 服务名称: emotion-single");
System.out.println(" - 服务端口: 8080");
System.out.println(" - 服务端口: 19089");
System.out.println(" - 环境配置: " + System.getProperty("spring.profiles.active"));
System.out.println(" - API文档: http://localhost:8080/api/health");
System.out.println(" - API文档: http://localhost:19089/api/health");
System.out.println("========================================");
}
}
@@ -39,18 +39,14 @@ public class SecurityConfig {
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用CSRF
.csrf().disable()
.csrf(csrf -> csrf.disable())
// 配置CORS
.cors().configurationSource(corsConfigurationSource())
.and()
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// 配置会话管理
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.sessionManagement(management -> management
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 配置授权规则
.authorizeHttpRequests(authz -> authz
@@ -58,10 +54,10 @@ public class SecurityConfig {
.anyRequest().permitAll())
// 禁用默认登录页面
.formLogin().disable()
.formLogin(login -> login.disable())
// 禁用HTTP Basic认证
.httpBasic().disable();
.httpBasic(basic -> basic.disable());
return http.build();
}
@@ -1,6 +1,9 @@
package com.emotion.config;
import com.emotion.interceptor.WebSocketAuthInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
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.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@@ -16,6 +19,9 @@ import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerCo
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Autowired
private WebSocketAuthInterceptor webSocketAuthInterceptor;
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 启用简单消息代理,并设置消息代理的前缀
@@ -39,4 +45,10 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
registry.addEndpoint("/ws/chat")
.setAllowedOriginPatterns("*");
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
// 添加WebSocket认证拦截器
registration.interceptors(webSocketAuthInterceptor);
}
}
@@ -0,0 +1,124 @@
package com.emotion.interceptor;
import com.emotion.service.AuthService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.security.Principal;
import java.util.Collections;
/**
* WebSocket认证拦截器
* 用于在WebSocket连接时验证用户身份
*
* @author emotion-museum
* @date 2025-07-24
*/
@Slf4j
@Component
public class WebSocketAuthInterceptor implements ChannelInterceptor {
@Autowired
private AuthService authService;
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
// 处理WebSocket连接时的认证
handleAuthentication(accessor);
}
return message;
}
/**
* 处理WebSocket连接认证
*/
private void handleAuthentication(StompHeaderAccessor accessor) {
try {
// 从连接头中获取token
String authHeader = accessor.getFirstNativeHeader("Authorization");
String userId = accessor.getFirstNativeHeader("X-User-Id");
log.info("WebSocket连接认证: authHeader={}, userId={}",
authHeader != null ? "Bearer ***" : null, userId);
if (StringUtils.hasText(authHeader) && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
// 验证token
if (authService.validateToken(token)) {
String tokenUserId = authService.getUserIdFromToken(token);
String username = authService.getUsernameFromToken(token);
log.info("WebSocket token验证成功: userId={}, username={}", tokenUserId, username);
// 创建认证对象
Authentication authentication = new UsernamePasswordAuthenticationToken(
tokenUserId,
null,
Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
);
// 设置用户认证信息
accessor.setUser(authentication);
// 设置会话属性
accessor.getSessionAttributes().put("userId", tokenUserId);
accessor.getSessionAttributes().put("username", username);
accessor.getSessionAttributes().put("authenticated", true);
} else {
log.warn("WebSocket token验证失败: token无效");
// token无效,但不阻止连接,作为访客处理
handleGuestUser(accessor, userId);
}
} else {
log.info("WebSocket连接无token,作为访客处理: userId={}", userId);
// 无token,作为访客处理
handleGuestUser(accessor, userId);
}
} catch (Exception e) {
log.error("WebSocket认证处理失败", e);
// 认证失败,作为访客处理
handleGuestUser(accessor, accessor.getFirstNativeHeader("X-User-Id"));
}
}
/**
* 处理访客用户
*/
private void handleGuestUser(StompHeaderAccessor accessor, String userId) {
String guestId = StringUtils.hasText(userId) ? userId : "guest_" + System.currentTimeMillis();
log.info("设置访客用户: guestId={}", guestId);
// 创建访客认证对象
Authentication guestAuth = new UsernamePasswordAuthenticationToken(
guestId,
null,
Collections.singletonList(new SimpleGrantedAuthority("ROLE_GUEST"))
);
accessor.setUser(guestAuth);
// 设置会话属性
accessor.getSessionAttributes().put("userId", guestId);
accessor.getSessionAttributes().put("username", guestId);
accessor.getSessionAttributes().put("authenticated", false);
}
}
@@ -43,22 +43,41 @@ public class WebSocketService {
*/
public void handleChatMessage(ChatRequest request, String sessionId, Principal principal) {
try {
log.info("处理聊天消息: {}", request);
log.info("处理聊天消息: request={}, sessionId={}, principal={}", request, sessionId, principal);
// 验证请求参数
if (request.getContent() == null || request.getContent().trim().isEmpty()) {
sendErrorMessage(request.getSenderId(), "消息内容不能为空");
return;
}
// 确定用户身份和类型
String userId = request.getSenderId();
WebSocketMessage.SenderType senderType = WebSocketMessage.SenderType.GUEST;
if (principal != null) {
userId = principal.getName();
// 如果用户ID不是以guest_开头,说明是认证用户
if (!userId.startsWith("guest_")) {
senderType = WebSocketMessage.SenderType.USER;
}
}
// 更新请求中的用户信息
request.setSenderId(userId);
request.setSenderType(senderType == WebSocketMessage.SenderType.USER ? ChatRequest.SenderType.USER
: ChatRequest.SenderType.GUEST);
log.info("确定用户身份: userId={}, senderType={}", userId, senderType);
// 构建用户消息
WebSocketMessage userMessage = WebSocketMessage.builder()
.messageId(UUID.randomUUID().toString())
.conversationId(request.getConversationId())
.type(WebSocketMessage.MessageType.TEXT)
.content(request.getContent())
.senderId(request.getSenderId())
.senderType(WebSocketMessage.SenderType.valueOf(request.getSenderType().name()))
.senderId(userId)
.senderType(senderType)
.status(WebSocketMessage.MessageStatus.SENT)
.createTime(LocalDateTime.now())
.build();
@@ -89,14 +108,22 @@ public class WebSocketService {
public void handleUserConnect(ConnectRequest request, String sessionId, Principal principal) {
try {
String userId = request.getUserId();
if (userId == null && principal != null) {
boolean isAuthenticated = false;
// 优先从Principal获取认证用户信息
if (principal != null) {
userId = principal.getName();
// 检查是否是认证用户(不是访客)
isAuthenticated = !userId.startsWith("guest_");
}
// 如果还没有userId,生成访客ID
if (userId == null) {
userId = "guest_" + sessionId;
}
log.info("用户连接WebSocket: userId={}, sessionId={}", userId, sessionId);
log.info("用户连接WebSocket: userId={}, sessionId={}, authenticated={}",
userId, sessionId, isAuthenticated);
// 记录在线用户
onlineUsers.put(sessionId, userId);